25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-24 16:23:40 +01:00

Merge branch 'master' into xml-ng

Conflicts:
	src/adhoc.erl
	src/cyrsasl_oauth.erl
	src/ejabberd_c2s.erl
	src/ejabberd_config.erl
	src/ejabberd_service.erl
	src/gen_mod.erl
	src/mod_admin_extra.erl
	src/mod_announce.erl
	src/mod_carboncopy.erl
	src/mod_client_state.erl
	src/mod_configure.erl
	src/mod_echo.erl
	src/mod_mam.erl
	src/mod_muc.erl
	src/mod_muc_room.erl
	src/mod_offline.erl
	src/mod_pubsub.erl
	src/mod_stats.erl
	src/node_flat_sql.erl
	src/randoms.erl
This commit is contained in:
Evgeniy Khramtsov 2016-11-12 13:27:15 +03:00
commit 78a44e0176
124 changed files with 7141 additions and 1335 deletions

View File

@ -211,6 +211,11 @@ install: all copy-files
> ejabberd.init
chmod 755 ejabberd.init
#
# Service script
$(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*" ejabberd.service.template \
> ejabberd.service
chmod 755 ejabberd.service
#
# Spool directory
$(INSTALL) -d -m 750 $(O_USER) $(SPOOLDIR)
$(CHOWN_COMMAND) -R @INSTALLUSER@ $(SPOOLDIR) >$(CHOWN_OUTPUT)

169
config/ejabberd.exs Normal file
View File

@ -0,0 +1,169 @@
defmodule Ejabberd.ConfigFile do
use Ejabberd.Config
def start do
[loglevel: 4,
log_rotate_size: 10485760,
log_rotate_date: "",
log_rotate_count: 1,
log_rate_limit: 100,
auth_method: :internal,
max_fsm_queue: 1000,
language: "en",
allow_contrib_modules: true,
hosts: ["localhost"],
shaper: shaper,
acl: acl,
access: access]
end
defp shaper do
[normal: 1000,
fast: 50000,
max_fsm_queue: 1000]
end
defp acl do
[local:
[user_regexp: "", loopback: [ip: "127.0.0.0/8"]]]
end
defp access do
[max_user_sessions: [all: 10],
max_user_offline_messages: [admin: 5000, all: 100],
local: [local: :allow],
c2s: [blocked: :deny, all: :allow],
c2s_shaper: [admin: :none, all: :normal],
s2s_shaper: [all: :fast],
announce: [admin: :allow],
configure: [admin: :allow],
muc_admin: [admin: :allow],
muc_create: [local: :allow],
muc: [all: :allow],
pubsub_createnode: [local: :allow],
register: [all: :allow],
trusted_network: [loopback: :allow]]
end
listen :ejabberd_c2s do
@opts [
port: 5222,
max_stanza_size: 65536,
shaper: :c2s_shaper,
access: :c2s]
end
listen :ejabberd_s2s_in do
@opts [port: 5269]
end
listen :ejabberd_http do
@opts [
port: 5280,
web_admin: true,
http_poll: true,
http_bind: true,
captcha: true]
end
module :mod_adhoc do
end
module :mod_announce do
@opts [access: :announce]
end
module :mod_blocking do
end
module :mod_caps do
end
module :mod_carboncopy do
end
module :mod_client_state do
@opts [
drop_chat_states: true,
queue_presence: false]
end
module :mod_configure do
end
module :mod_disco do
end
module :mod_irc do
end
module :mod_http_bind do
end
module :mod_last do
end
module :mod_muc do
@opts [
access: :muc,
access_create: :muc_create,
access_persistent: :muc_create,
access_admin: :muc_admin]
end
module :mod_offline do
@opts [access_max_user_messages: :max_user_offline_messages]
end
module :mod_ping do
end
module :mod_privacy do
end
module :mod_private do
end
module :mod_pubsub do
@opts [
access_createnode: :pubsub_createnode,
ignore_pep_from_offline: true,
last_item_cache: true,
plugins: ["flat", "hometree", "pep"]]
end
module :mod_register do
@opts [welcome_message: [
subject: "Welcome!",
body: "Hi.\nWelcome to this XMPP Server",
ip_access: :trusted_network,
access: :register]]
end
module :mod_roster do
end
module :mod_shared_roster do
end
module :mod_stats do
end
module :mod_time do
end
module :mod_version do
end
# Example of how to define a hook, called when the event
# specified is triggered.
#
# @event: Name of the event
# @opts: Params are optional. Available: :host and :priority.
# If missing, defaults are used. (host: :global | priority: 50)
# @callback Could be an anonymous function or a callback from a module,
# use the &ModuleName.function/arity format for that.
hook :register_user, [host: "localhost"], fn(user, server) ->
info("User registered: #{user} on #{server}")
end
end

667
config/ejabberd.yml Normal file
View File

@ -0,0 +1,667 @@
###
### ejabberd configuration file
###
###
### The parameters used in this configuration file are explained in more detail
### in the ejabberd Installation and Operation Guide.
### Please consult the Guide in case of doubts, it is included with
### your copy of ejabberd, and is also available online at
### http://www.process-one.net/en/ejabberd/docs/
### The configuration file is written in YAML.
### Refer to http://en.wikipedia.org/wiki/YAML for the brief description.
### However, ejabberd treats different literals as different types:
###
### - unquoted or single-quoted strings. They are called "atoms".
### Example: dog, 'Jupiter', '3.14159', YELLOW
###
### - numeric literals. Example: 3, -45.0, .0
###
### - quoted or folded strings.
### Examples of quoted string: "Lizzard", "orange".
### Example of folded string:
### > Art thou not Romeo,
### and a Montague?
### =======
### LOGGING
##
## loglevel: Verbosity of log files generated by ejabberd.
## 0: No ejabberd log at all (not recommended)
## 1: Critical
## 2: Error
## 3: Warning
## 4: Info
## 5: Debug
##
loglevel: 4
##
## rotation: Describe how to rotate logs. Either size and/or date can trigger
## log rotation. Setting count to N keeps N rotated logs. Setting count to 0
## does not disable rotation, it instead rotates the file and keeps no previous
## versions around. Setting size to X rotate log when it reaches X bytes.
## To disable rotation set the size to 0 and the date to ""
## Date syntax is taken from the syntax newsyslog uses in newsyslog.conf.
## Some examples:
## $D0 rotate every night at midnight
## $D23 rotate every day at 23:00 hr
## $W0D23 rotate every week on Sunday at 23:00 hr
## $W5D16 rotate every week on Friday at 16:00 hr
## $M1D0 rotate on the first day of every month at midnight
## $M5D6 rotate on every 5th day of the month at 6:00 hr
##
log_rotate_size: 10485760
log_rotate_date: ""
log_rotate_count: 1
##
## overload protection: If you want to limit the number of messages per second
## allowed from error_logger, which is a good idea if you want to avoid a flood
## of messages when system is overloaded, you can set a limit.
## 100 is ejabberd's default.
log_rate_limit: 100
##
## watchdog_admins: Only useful for developers: if an ejabberd process
## consumes a lot of memory, send live notifications to these XMPP
## accounts.
##
## watchdog_admins:
## - "bob@example.com"
### ================
### SERVED HOSTNAMES
##
## hosts: Domains served by ejabberd.
## You can define one or several, for example:
## hosts:
## - "example.net"
## - "example.com"
## - "example.org"
##
hosts:
- "localhost"
##
## route_subdomains: Delegate subdomains to other XMPP servers.
## For example, if this ejabberd serves example.org and you want
## to allow communication with an XMPP server called im.example.org.
##
## route_subdomains: s2s
### ===============
### LISTENING PORTS
##
## listen: The ports ejabberd will listen on, which service each is handled
## by and what options to start it with.
##
listen:
-
port: 5222
module: ejabberd_c2s
##
## If TLS is compiled in and you installed a SSL
## certificate, specify the full path to the
## file and uncomment these lines:
##
## certfile: "/path/to/ssl.pem"
## starttls: true
##
## To enforce TLS encryption for client connections,
## use this instead of the "starttls" option:
##
## starttls_required: true
##
## Custom OpenSSL options
##
## protocol_options:
## - "no_sslv3"
## - "no_tlsv1"
max_stanza_size: 65536
shaper: c2s_shaper
access: c2s
-
port: 5269
module: ejabberd_s2s_in
##
## ejabberd_service: Interact with external components (transports, ...)
##
## -
## port: 8888
## module: ejabberd_service
## access: all
## shaper_rule: fast
## ip: "127.0.0.1"
## hosts:
## "icq.example.org":
## password: "secret"
## "sms.example.org":
## password: "secret"
##
## ejabberd_stun: Handles STUN Binding requests
##
## -
## port: 3478
## transport: udp
## module: ejabberd_stun
##
## To handle XML-RPC requests that provide admin credentials:
##
## -
## port: 4560
## module: ejabberd_xmlrpc
-
port: 5280
module: ejabberd_http
## request_handlers:
## "/pub/archive": mod_http_fileserver
web_admin: true
http_poll: true
http_bind: true
## register: true
captcha: true
##
## s2s_use_starttls: Enable STARTTLS + Dialback for S2S connections.
## Allowed values are: false optional required required_trusted
## You must specify a certificate file.
##
## s2s_use_starttls: optional
##
## s2s_certfile: Specify a certificate file.
##
## s2s_certfile: "/path/to/ssl.pem"
## Custom OpenSSL options
##
## s2s_protocol_options:
## - "no_sslv3"
## - "no_tlsv1"
##
## domain_certfile: Specify a different certificate for each served hostname.
##
## host_config:
## "example.org":
## domain_certfile: "/path/to/example_org.pem"
## "example.com":
## domain_certfile: "/path/to/example_com.pem"
##
## S2S whitelist or blacklist
##
## Default s2s policy for undefined hosts.
##
## s2s_access: s2s
##
## Outgoing S2S options
##
## Preferred address families (which to try first) and connect timeout
## in milliseconds.
##
## outgoing_s2s_families:
## - ipv4
## - ipv6
## outgoing_s2s_timeout: 10000
### ==============
### AUTHENTICATION
##
## auth_method: Method used to authenticate the users.
## The default method is the internal.
## If you want to use a different method,
## comment this line and enable the correct ones.
##
auth_method: internal
##
## Store the plain passwords or hashed for SCRAM:
## auth_password_format: plain
## auth_password_format: scram
##
## Define the FQDN if ejabberd doesn't detect it:
## fqdn: "server3.example.com"
##
## Authentication using external script
## Make sure the script is executable by ejabberd.
##
## auth_method: external
## extauth_program: "/path/to/authentication/script"
##
## Authentication using ODBC
## Remember to setup a database in the next section.
##
## auth_method: odbc
##
## Authentication using PAM
##
## auth_method: pam
## pam_service: "pamservicename"
##
## Authentication using LDAP
##
## auth_method: ldap
##
## List of LDAP servers:
## ldap_servers:
## - "localhost"
##
## Encryption of connection to LDAP servers:
## ldap_encrypt: none
## ldap_encrypt: tls
##
## Port to connect to on LDAP servers:
## ldap_port: 389
## ldap_port: 636
##
## LDAP manager:
## ldap_rootdn: "dc=example,dc=com"
##
## Password of LDAP manager:
## ldap_password: "******"
##
## Search base of LDAP directory:
## ldap_base: "dc=example,dc=com"
##
## LDAP attribute that holds user ID:
## ldap_uids:
## - "mail": "%u@mail.example.org"
##
## LDAP filter:
## ldap_filter: "(objectClass=shadowAccount)"
##
## Anonymous login support:
## auth_method: anonymous
## anonymous_protocol: sasl_anon | login_anon | both
## allow_multiple_connections: true | false
##
## host_config:
## "public.example.org":
## auth_method: anonymous
## allow_multiple_connections: false
## anonymous_protocol: sasl_anon
##
## To use both anonymous and internal authentication:
##
## host_config:
## "public.example.org":
## auth_method:
## - internal
## - anonymous
### ==============
### DATABASE SETUP
## ejabberd by default uses the internal Mnesia database,
## so you do not necessarily need this section.
## This section provides configuration examples in case
## you want to use other database backends.
## Please consult the ejabberd Guide for details on database creation.
##
## MySQL server:
##
## odbc_type: mysql
## odbc_server: "server"
## odbc_database: "database"
## odbc_username: "username"
## odbc_password: "password"
##
## If you want to specify the port:
## odbc_port: 1234
##
## PostgreSQL server:
##
## odbc_type: pgsql
## odbc_server: "server"
## odbc_database: "database"
## odbc_username: "username"
## odbc_password: "password"
##
## If you want to specify the port:
## odbc_port: 1234
##
## If you use PostgreSQL, have a large database, and need a
## faster but inexact replacement for "select count(*) from users"
##
## pgsql_users_number_estimate: true
##
## ODBC compatible or MSSQL server:
##
## odbc_type: odbc
## odbc_server: "DSN=ejabberd;UID=ejabberd;PWD=ejabberd"
##
## Number of connections to open to the database for each virtual host
##
## odbc_pool_size: 10
##
## Interval to make a dummy SQL request to keep the connections to the
## database alive. Specify in seconds: for example 28800 means 8 hours
##
## odbc_keepalive_interval: undefined
### ===============
### TRAFFIC SHAPERS
shaper:
##
## The "normal" shaper limits traffic speed to 1000 B/s
##
normal: 1000
##
## The "fast" shaper limits traffic speed to 50000 B/s
##
fast: 50000
##
## This option specifies the maximum number of elements in the queue
## of the FSM. Refer to the documentation for details.
##
max_fsm_queue: 1000
###. ====================
###' ACCESS CONTROL LISTS
acl:
##
## The 'admin' ACL grants administrative privileges to XMPP accounts.
## You can put here as many accounts as you want.
##
## admin:
## user:
## - "aleksey": "localhost"
## - "ermine": "example.org"
##
## Blocked users
##
## blocked:
## user:
## - "baduser": "example.org"
## - "test"
## Local users: don't modify this.
##
local:
user_regexp: ""
##
## More examples of ACLs
##
## jabberorg:
## server:
## - "jabber.org"
## aleksey:
## user:
## - "aleksey": "jabber.ru"
## test:
## user_regexp: "^test"
## user_glob: "test*"
##
## Loopback network
##
loopback:
ip:
- "127.0.0.0/8"
##
## Bad XMPP servers
##
## bad_servers:
## server:
## - "xmpp.zombie.org"
## - "xmpp.spam.com"
##
## Define specific ACLs in a virtual host.
##
## host_config:
## "localhost":
## acl:
## admin:
## user:
## - "bob-local": "localhost"
### ============
### ACCESS RULES
access:
## Maximum number of simultaneous sessions allowed for a single user:
max_user_sessions:
all: 10
## Maximum number of offline messages that users can have:
max_user_offline_messages:
admin: 5000
all: 100
## This rule allows access only for local users:
local:
local: allow
## Only non-blocked users can use c2s connections:
c2s:
blocked: deny
all: allow
## For C2S connections, all users except admins use the "normal" shaper
c2s_shaper:
admin: none
all: normal
## All S2S connections use the "fast" shaper
s2s_shaper:
all: fast
## Only admins can send announcement messages:
announce:
admin: allow
## Only admins can use the configuration interface:
configure:
admin: allow
## Admins of this server are also admins of the MUC service:
muc_admin:
admin: allow
## Only accounts of the local ejabberd server can create rooms:
muc_create:
local: allow
## All users are allowed to use the MUC service:
muc:
all: allow
## Only accounts on the local ejabberd server can create Pubsub nodes:
pubsub_createnode:
local: allow
## In-band registration allows registration of any possible username.
## To disable in-band registration, replace 'allow' with 'deny'.
register:
all: allow
## Only allow to register from localhost
trusted_network:
loopback: allow
## Do not establish S2S connections with bad servers
## s2s:
## bad_servers: deny
## all: allow
## By default the frequency of account registrations from the same IP
## is limited to 1 account every 10 minutes. To disable, specify: infinity
## registration_timeout: 600
##
## Define specific Access Rules in a virtual host.
##
## host_config:
## "localhost":
## access:
## c2s:
## admin: allow
## all: deny
## register:
## all: deny
### ================
### DEFAULT LANGUAGE
##
## language: Default language used for server messages.
##
language: "en"
##
## Set a different default language in a virtual host.
##
## host_config:
## "localhost":
## language: "ru"
### =======
### CAPTCHA
##
## Full path to a script that generates the image.
##
## captcha_cmd: "/lib/ejabberd/priv/bin/captcha.sh"
##
## Host for the URL and port where ejabberd listens for CAPTCHA requests.
##
## captcha_host: "example.org:5280"
##
## Limit CAPTCHA calls per minute for JID/IP to avoid DoS.
##
## captcha_limit: 5
### =======
### MODULES
##
## Modules enabled in all ejabberd virtual hosts.
##
modules:
mod_adhoc: {}
## mod_admin_extra: {}
mod_announce: # recommends mod_adhoc
access: announce
mod_blocking: {} # requires mod_privacy
mod_caps: {}
mod_carboncopy: {}
mod_client_state:
drop_chat_states: true
queue_presence: false
mod_configure: {} # requires mod_adhoc
mod_disco: {}
## mod_echo: {}
mod_irc: {}
mod_http_bind: {}
## mod_http_fileserver:
## docroot: "/var/www"
## accesslog: "/var/log/ejabberd/access.log"
mod_last: {}
mod_muc:
## host: "conference.@HOST@"
access: muc
access_create: muc_create
access_persistent: muc_create
access_admin: muc_admin
## mod_muc_log: {}
mod_offline:
access_max_user_messages: max_user_offline_messages
mod_ping: {}
## mod_pres_counter:
## count: 5
## interval: 60
mod_privacy: {}
mod_private: {}
## mod_proxy65: {}
mod_pubsub:
access_createnode: pubsub_createnode
## reduces resource comsumption, but XEP incompliant
ignore_pep_from_offline: true
## XEP compliant, but increases resource comsumption
## ignore_pep_from_offline: false
last_item_cache: false
plugins:
- "flat"
- "hometree"
- "pep" # pep requires mod_caps
mod_register:
##
## Protect In-Band account registrations with CAPTCHA.
##
## captcha_protected: true
##
## Set the minimum informational entropy for passwords.
##
## password_strength: 32
##
## After successful registration, the user receives
## a message with this subject and body.
##
welcome_message:
subject: "Welcome!"
body: |-
Hi.
Welcome to this XMPP server.
##
## When a user registers, send a notification to
## these XMPP accounts.
##
## registration_watchers:
## - "admin1@example.org"
##
## Only clients in the server machine can register accounts
##
ip_access: trusted_network
##
## Local c2s or remote s2s users cannot register accounts
##
## access_from: deny
access: register
mod_roster: {}
mod_shared_roster: {}
mod_stats: {}
mod_time: {}
mod_vcard: {}
mod_version: {}
##
## Enable modules with custom options in a specific virtual host
##
## host_config:
## "localhost":
## modules:
## mod_echo:
## host: "mirror.localhost"
##
## Enable modules management via ejabberdctl for installation and
## uninstallation of public/private contributed modules
## (enabled by default)
##
allow_contrib_modules: true
### Local Variables:
### mode: yaml
### End:
### vim: set filetype=yaml tabstop=8

View File

@ -12,6 +12,13 @@ ExecStop=@ctlscriptpath@/ejabberdctl stop
ExecReload=@ctlscriptpath@/ejabberdctl reload_config
Type=oneshot
RemainAfterExit=yes
# The CAP_DAC_OVERRIDE capability is required for pam authentication to work
CapabilityBoundingSet=CAP_DAC_OVERRIDE
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=full
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target

View File

@ -147,6 +147,15 @@ listen:
## access: all
## shaper_rule: fast
## ip: "127.0.0.1"
## privilege_access:
## roster: "both"
## message: "outgoing"
## presence: "roster"
## delegations:
## "urn:xmpp:mam:1":
## filtering: ["node"]
## "http://jabber.org/protocol/pubsub":
## filtering: []
## hosts:
## "icq.example.org":
## password: "secret"
@ -580,6 +589,7 @@ modules:
mod_carboncopy: {}
mod_client_state: {}
mod_configure: {} # requires mod_adhoc
##mod_delegation: {} # for xep0356
mod_disco: {}
## mod_echo: {}
mod_irc: {}

View File

@ -26,6 +26,25 @@
{tuple, [rterm()]} | {list, rterm()} |
rescode | restuple.
-type oauth_scope() :: atom().
%% ejabberd_commands OAuth ReST ACL definition:
%% Two fields exist that are used to control access on a command from ReST API:
%% 1. Policy
%% If policy is:
%% - restricted: command is not exposed as OAuth Rest API.
%% - admin: Command is allowed for user that have Admin Rest command enabled by access rule: commands_admin_access
%% - user: Command might be called by any server user.
%% - open: Command can be called by anyone.
%%
%% Policy is just used to control who can call the command. A specific additional access rules can be performed, as
%% defined by access option.
%% Access option can be a list of:
%% - {Module, accessName, DefaultValue}: Reference and existing module access to limit who can use the command.
%% - AccessRule name: direct name of the access rule to check in config file.
%% TODO: Access option could be atom command (not a list). In the case, User performing the command, will be added as first parameter
%% to command, so that the command can perform additional check.
-record(ejabberd_commands,
{name :: atom(),
tags = [] :: [atom()] | '_' | '$2',
@ -36,19 +55,25 @@
function :: atom() | '_',
args = [] :: [aterm()] | '_' | '$1' | '$2',
policy = restricted :: open | restricted | admin | user,
%% access is: [accessRuleName] or [{Module, AccessOption, DefaultAccessRuleName}]
access = [] :: [{atom(),atom(),atom()}|atom()],
result = {res, rescode} :: rterm() | '_' | '$2',
args_desc = none :: none | [string()] | '_',
result_desc = none :: none | string() | '_',
args_example = none :: none | [any()] | '_',
result_example = none :: any()}).
%% TODO Fix me: Type is not up to date
-type ejabberd_commands() :: #ejabberd_commands{name :: atom(),
tags :: [atom()],
desc :: string(),
longdesc :: string(),
version :: integer(),
module :: atom(),
function :: atom(),
args :: [aterm()],
policy :: open | restricted | admin | user,
access :: [{atom(),atom(),atom()}|atom()],
result :: rterm()}.
%% @type ejabberd_commands() = #ejabberd_commands{

View File

@ -0,0 +1,26 @@
%%%----------------------------------------------------------------------
%%%
%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
-record(oauth_token, {
token = <<"">> :: binary() | '_',
us = {<<"">>, <<"">>} :: {binary(), binary()} | '_',
scope = [] :: [binary()] | '_',
expire :: integer() | '$1'
}).

View File

@ -0,0 +1,20 @@
-include("ejabberd.hrl").
-include("logger.hrl").
-include("jlib.hrl").
-type filter_attr() :: {binary(), [binary()]}.
-record(state,
{socket :: ejabberd_socket:socket_state(),
sockmod = ejabberd_socket :: ejabberd_socket | ejabberd_frontend_socket,
streamid = <<"">> :: binary(),
host_opts = dict:new() :: ?TDICT,
host = <<"">> :: binary(),
access :: atom(),
check_from = true :: boolean(),
server_hosts = ?MYHOSTS :: [binary()],
privilege_access :: [attr()],
delegations :: [filter_attr()],
last_pres = dict:new() :: ?TDICT}).
-type(state() :: #state{} ).

View File

@ -1,9 +1,9 @@
-ifndef(EJABBERD_SM_HRL).
-define(EJABBERD_SM_HRL, true).
-record(session, {sid, usr, us, priority, info}).
-record(session, {sid, usr, us, priority, info = []}).
-record(session_counter, {vhost, count}).
-type sid() :: {erlang:timestamp(), pid()} | {erlang:timestamp(), undefined}.
-type sid() :: {erlang:timestamp(), pid()}.
-type ip() :: {inet:ip_address(), inet:port_number()} | undefined.
-type info() :: [{conn, atom()} | {ip, ip()} | {node, atom()}
| {oor, boolean()} | {auth_module, atom()}

View File

@ -78,11 +78,15 @@
jid :: jid(),
nick :: binary(),
role :: role(),
is_subscriber = false :: boolean(),
subscriptions = [] :: [binary()],
%%is_subscriber = false :: boolean(),
%%subscriptions = [] :: [binary()],
last_presence :: xmlel()
}).
-record(subscriber, {jid :: jid(),
nick = <<>> :: binary(),
nodes = [] :: [binary()]}).
-record(activity,
{
message_time = 0 :: integer(),
@ -102,6 +106,8 @@
jid = #jid{} :: jid(),
config = #config{} :: config(),
users = (?DICT):new() :: ?TDICT,
subscribers = (?DICT):new() :: ?TDICT,
subscriber_nicks = (?DICT):new() :: ?TDICT,
last_voice_request_time = treap:empty() :: treap:treap(),
robots = (?DICT):new() :: ?TDICT,
nicks = (?DICT):new() :: ?TDICT,

View File

@ -170,6 +170,8 @@
-define(NS_MIX_NODES_PARTICIPANTS, <<"urn:xmpp:mix:nodes:participants">>).
-define(NS_MIX_NODES_SUBJECT, <<"urn:xmpp:mix:nodes:subject">>).
-define(NS_MIX_NODES_CONFIG, <<"urn:xmpp:mix:nodes:config">>).
-define(NS_PRIVILEGE, <<"urn:xmpp:privilege:1">>).
-define(NS_DELEGATION, <<"urn:xmpp:delegation:1">>).
-define(NS_MUCSUB, <<"urn:xmpp:mucsub:0">>).
-define(NS_MUCSUB_NODES_PRESENCE, <<"urn:xmpp:mucsub:nodes:presence">>).
-define(NS_MUCSUB_NODES_MESSAGES, <<"urn:xmpp:mucsub:nodes:messages">>).

View File

@ -146,6 +146,10 @@
height :: non_neg_integer()}).
-type thumbnail() :: #thumbnail{}.
-record(privilege_perm, {access :: 'message' | 'presence' | 'roster',
type :: 'both' | 'get' | 'managed_entity' | 'none' | 'outgoing' | 'roster' | 'set'}).
-type privilege_perm() :: #privilege_perm{}.
-record(muc_decline, {reason = <<>> :: binary(),
from :: jid:jid(),
to :: jid:jid()}).
@ -176,6 +180,14 @@
-record(starttls_proceed, {}).
-type starttls_proceed() :: #starttls_proceed{}.
-record(forwarded, {delay :: #delay{},
sub_els = [] :: [xmpp_element() | fxml:xmlel()]}).
-type forwarded() :: #forwarded{}.
-record(privilege, {perms = [] :: [#privilege_perm{}],
forwarded :: #forwarded{}}).
-type privilege() :: #privilege{}.
-record(client_id, {id = <<>> :: binary()}).
-type client_id() :: #client_id{}.
@ -184,10 +196,6 @@
xmlns = <<>> :: binary()}).
-type sm_resumed() :: #sm_resumed{}.
-record(forwarded, {delay :: #delay{},
sub_els = [] :: [xmpp_element() | fxml:xmlel()]}).
-type forwarded() :: #forwarded{}.
-record(sm_enable, {max :: non_neg_integer(),
resume = false :: boolean(),
xmlns = <<>> :: binary()}).
@ -215,6 +223,10 @@
-record(private, {xml_els = [] :: [fxml:xmlel()]}).
-type private() :: #private{}.
-record(delegation_query, {to :: jid:jid(),
delegate = [] :: [binary()]}).
-type delegation_query() :: #delegation_query{}.
-record(db_verify, {from = <<>> :: binary(),
to = <<>> :: binary(),
id = <<>> :: binary(),
@ -534,6 +546,10 @@
continue :: binary()}).
-type muc_invite() :: #muc_invite{}.
-record(delegated, {ns = <<>> :: binary(),
attrs = [] :: [binary()]}).
-type delegated() :: #delegated{}.
-record(carbons_disable, {}).
-type carbons_disable() :: #carbons_disable{}.
@ -838,6 +854,10 @@
sub_els = [] :: [xmpp_element() | fxml:xmlel()]}).
-type stanza_error() :: #stanza_error{}.
-record(delegation, {delegated = [] :: [#delegated{}],
forwarded :: #forwarded{}}).
-type delegation() :: #delegation{}.
-record(mix_join, {jid :: jid:jid(),
subscribe = [] :: [binary()]}).
-type mix_join() :: #mix_join{}.
@ -905,21 +925,18 @@
utc :: erlang:timestamp()}).
-type time() :: #time{}.
-type xmpp_element() :: muc_admin() |
compression() |
-type xmpp_element() :: compression() |
ps_subscription() |
xdata_option() |
version() |
ps_affiliation() |
mam_fin() |
sm_a() |
bob_data() |
media() |
stanza_id() |
starttls_proceed() |
forwarded() |
client_id() |
sm_resumed() |
forwarded() |
xevent() |
privacy_list() |
carbons_sent() |
@ -932,6 +949,7 @@
mix_participant() |
compressed() |
block_list() |
delegated() |
rsm_set() |
'see-other-host'() |
hint() |
@ -953,10 +971,10 @@
compress() |
bytestreams() |
adhoc_actions() |
privacy_query() |
muc_history() |
identity() |
feature_csi() |
privacy_query() |
delay() |
thumbnail() |
vcard_tel() |
@ -993,6 +1011,7 @@
nick() |
p1_ack() |
block() |
delegation() |
mix_join() |
xmpp_session() |
xdata() |
@ -1014,6 +1033,7 @@
adhoc_command() |
sm_failed() |
ping() |
privilege_perm() |
privacy_item() |
disco_item() |
ps_item() |
@ -1027,12 +1047,13 @@
sic() |
ps_options() |
starttls() |
db_verify() |
roster_query() |
media_uri() |
muc_destroy() |
vcard_key() |
csi() |
db_verify() |
roster_query() |
delegation_query() |
mam_query() |
bookmark_url() |
vcard_email() |
@ -1051,6 +1072,7 @@
carbons_private() |
mix_leave() |
muc_subscribe() |
privilege() |
muc_unique() |
sasl_response() |
message() |
@ -1064,4 +1086,7 @@
sasl_auth() |
p1_push() |
oob_x() |
unblock().
unblock() |
muc_admin() |
ps_affiliation() |
mam_fin().

View File

@ -3,7 +3,7 @@ defmodule ExUnit.CTFormatter do
use GenEvent
import ExUnit.Formatter, only: [format_time: 2, format_filters: 2, format_test_failure: 5,
import ExUnit.Formatter, only: [format_time: 2, format_test_failure: 5,
format_test_case_failure: 5]
def init(opts) do

119
lib/ejabberd/config/attr.ex Normal file
View File

@ -0,0 +1,119 @@
defmodule Ejabberd.Config.Attr do
@moduledoc """
Module used to work with the attributes parsed from
an elixir block (do...end).
Contains functions for extracting attrs from a block
and validation.
"""
@type attr :: {atom(), any()}
@attr_supported [
active:
[type: :boolean, default: true],
git:
[type: :string, default: ""],
name:
[type: :string, default: ""],
opts:
[type: :list, default: []],
dependency:
[type: :list, default: []]
]
@doc """
Takes a block with annotations and extracts the list
of attributes.
"""
@spec extract_attrs_from_block_with_defaults(any()) :: [attr]
def extract_attrs_from_block_with_defaults(block) do
block
|> extract_attrs_from_block
|> put_into_list_if_not_already
|> insert_default_attrs_if_missing
end
@doc """
Takes an attribute or a list of attrs and validate them.
Returns a {:ok, attr} or {:error, attr, cause} for each of the attributes.
"""
@spec validate([attr]) :: [{:ok, attr}] | [{:error, attr, atom()}]
def validate(attrs) when is_list(attrs), do: Enum.map(attrs, &valid_attr?/1)
def validate(attr), do: validate([attr]) |> List.first
@doc """
Returns the type of an attribute, given its name.
"""
@spec get_type_for_attr(atom()) :: atom()
def get_type_for_attr(attr_name) do
@attr_supported
|> Keyword.get(attr_name)
|> Keyword.get(:type)
end
@doc """
Returns the default value for an attribute, given its name.
"""
@spec get_default_for_attr(atom()) :: any()
def get_default_for_attr(attr_name) do
@attr_supported
|> Keyword.get(attr_name)
|> Keyword.get(:default)
end
# Private API
# Given an elixir block (do...end) returns a list with the annotations
# or a single annotation.
@spec extract_attrs_from_block(any()) :: [attr] | attr
defp extract_attrs_from_block({:__block__, [], attrs}), do: Enum.map(attrs, &extract_attrs_from_block/1)
defp extract_attrs_from_block({:@, _, [attrs]}), do: extract_attrs_from_block(attrs)
defp extract_attrs_from_block({attr_name, _, [value]}), do: {attr_name, value}
defp extract_attrs_from_block(nil), do: []
# In case extract_attrs_from_block returns a single attribute,
# then put it into a list. (Ensures attrs are always into a list).
@spec put_into_list_if_not_already([attr] | attr) :: [attr]
defp put_into_list_if_not_already(attrs) when is_list(attrs), do: attrs
defp put_into_list_if_not_already(attr), do: [attr]
# Given a list of attributes, it inserts the missing attribute with their
# default value.
@spec insert_default_attrs_if_missing([attr]) :: [attr]
defp insert_default_attrs_if_missing(attrs) do
Enum.reduce @attr_supported, attrs, fn({attr_name, _}, acc) ->
case Keyword.has_key?(acc, attr_name) do
true -> acc
false -> Keyword.put(acc, attr_name, get_default_for_attr(attr_name))
end
end
end
# Given an attribute, validates it and return a tuple with
# {:ok, attr} or {:error, attr, cause}
@spec valid_attr?(attr) :: {:ok, attr} | {:error, attr, atom()}
defp valid_attr?({attr_name, param} = attr) do
case Keyword.get(@attr_supported, attr_name) do
nil -> {:error, attr, :attr_not_supported}
[{:type, param_type} | _] -> case is_of_type?(param, param_type) do
true -> {:ok, attr}
false -> {:error, attr, :type_not_supported}
end
end
end
# Given an attribute value and a type, it returns a true
# if the value its of the type specified, false otherwise.
# Usefoul for checking if an attr value respects the type
# specified for the annotation.
@spec is_of_type?(any(), atom()) :: boolean()
defp is_of_type?(param, type) when type == :boolean and is_boolean(param), do: true
defp is_of_type?(param, type) when type == :string and is_bitstring(param), do: true
defp is_of_type?(param, type) when type == :list and is_list(param), do: true
defp is_of_type?(param, type) when type == :atom and is_atom(param), do: true
defp is_of_type?(_param, type) when type == :any, do: true
defp is_of_type?(_, _), do: false
end

View File

@ -0,0 +1,145 @@
defmodule Ejabberd.Config do
@moduledoc """
Base module for configuration file.
Imports macros for the config DSL and contains functions
for working/starting the configuration parsed.
"""
alias Ejabberd.Config.EjabberdModule
alias Ejabberd.Config.Attr
alias Ejabberd.Config.EjabberdLogger
defmacro __using__(_opts) do
quote do
import Ejabberd.Config, only: :macros
import Ejabberd.Logger
@before_compile Ejabberd.Config
end
end
# Validate the modules parsed and log validation errors at compile time.
# Could be also possible to interrupt the compilation&execution by throwing
# an exception if necessary.
def __before_compile__(_env) do
get_modules_parsed_in_order
|> EjabberdModule.validate
|> EjabberdLogger.log_errors
end
@doc """
Given the path of the config file, it evaluates it.
"""
def init(file_path, force \\ false) do
init_already_executed = Ejabberd.Config.Store.get(:module_name) != []
case force do
true ->
Ejabberd.Config.Store.stop
Ejabberd.Config.Store.start_link
do_init(file_path)
false ->
if not init_already_executed, do: do_init(file_path)
end
end
@doc """
Returns a list with all the opts, formatted for ejabberd.
"""
def get_ejabberd_opts do
get_general_opts
|> Dict.put(:modules, get_modules_parsed_in_order())
|> Dict.put(:listeners, get_listeners_parsed_in_order())
|> Ejabberd.Config.OptsFormatter.format_opts_for_ejabberd
end
@doc """
Register the hooks defined inside the elixir config file.
"""
def start_hooks do
get_hooks_parsed_in_order()
|> Enum.each(&Ejabberd.Config.EjabberdHook.start/1)
end
###
### MACROS
###
defmacro listen(module, do: block) do
attrs = Attr.extract_attrs_from_block_with_defaults(block)
quote do
Ejabberd.Config.Store.put(:listeners, %EjabberdModule{
module: unquote(module),
attrs: unquote(attrs)
})
end
end
defmacro module(module, do: block) do
attrs = Attr.extract_attrs_from_block_with_defaults(block)
quote do
Ejabberd.Config.Store.put(:modules, %EjabberdModule{
module: unquote(module),
attrs: unquote(attrs)
})
end
end
defmacro hook(hook_name, opts, fun) do
quote do
Ejabberd.Config.Store.put(:hooks, %Ejabberd.Config.EjabberdHook{
hook: unquote(hook_name),
opts: unquote(opts),
fun: unquote(fun)
})
end
end
# Private API
defp do_init(file_path) do
# File evaluation
Code.eval_file(file_path) |> extract_and_store_module_name()
# Getting start/0 config
Ejabberd.Config.Store.get(:module_name)
|> case do
nil -> IO.puts "[ ERR ] Configuration module not found."
[module] -> call_start_func_and_store_data(module)
end
# Fetching git modules and install them
get_modules_parsed_in_order()
|> EjabberdModule.fetch_git_repos
end
# Returns the modules from the store
defp get_modules_parsed_in_order,
do: Ejabberd.Config.Store.get(:modules) |> Enum.reverse
# Returns the listeners from the store
defp get_listeners_parsed_in_order,
do: Ejabberd.Config.Store.get(:listeners) |> Enum.reverse
defp get_hooks_parsed_in_order,
do: Ejabberd.Config.Store.get(:hooks) |> Enum.reverse
# Returns the general config options
defp get_general_opts,
do: Ejabberd.Config.Store.get(:general) |> List.first
# Gets the general ejabberd options calling
# the start/0 function and stores them.
defp call_start_func_and_store_data(module) do
opts = apply(module, :start, [])
Ejabberd.Config.Store.put(:general, opts)
end
# Stores the configuration module name
defp extract_and_store_module_name({{:module, mod, _bytes, _}, _}) do
Ejabberd.Config.Store.put(:module_name, mod)
end
end

View File

@ -0,0 +1,23 @@
defmodule Ejabberd.Config.EjabberdHook do
@moduledoc """
Module containing functions for manipulating
ejabberd hooks.
"""
defstruct hook: nil, opts: [], fun: nil
alias Ejabberd.Config.EjabberdHook
@type t :: %EjabberdHook{}
@doc """
Register a hook to ejabberd.
"""
@spec start(EjabberdHook.t) :: none
def start(%EjabberdHook{hook: hook, opts: opts, fun: fun}) do
host = Keyword.get(opts, :host, :global)
priority = Keyword.get(opts, :priority, 50)
:ejabberd_hooks.add(hook, host, fun, priority)
end
end

View File

@ -0,0 +1,70 @@
defmodule Ejabberd.Config.EjabberdModule do
@moduledoc """
Module representing a module block in the configuration file.
It offers functions for validation and for starting the modules.
Warning: The name is EjabberdModule to not collide with
the already existing Elixir.Module.
"""
@type t :: %{module: atom, attrs: [Attr.t]}
defstruct [:module, :attrs]
alias Ejabberd.Config.EjabberdModule
alias Ejabberd.Config.Attr
alias Ejabberd.Config.Validation
@doc """
Given a list of modules / single module
it runs different validators on them.
For each module, returns a {:ok, mod} or {:error, mod, errors}
"""
def validate(modules) do
Validation.validate(modules)
end
@doc """
Given a list of modules, it takes only the ones with
a git attribute and tries to fetch the repo,
then, it install them through :ext_mod.install/1
"""
@spec fetch_git_repos([EjabberdModule.t]) :: none()
def fetch_git_repos(modules) do
modules
|> Enum.filter(&is_git_module?/1)
|> Enum.each(&fetch_and_install_git_module/1)
end
# Private API
defp is_git_module?(%EjabberdModule{attrs: attrs}) do
case Keyword.get(attrs, :git) do
"" -> false
repo -> String.match?(repo, ~r/((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)(\/)?/)
end
end
defp fetch_and_install_git_module(%EjabberdModule{attrs: attrs}) do
repo = Keyword.get(attrs, :git)
mod_name = case Keyword.get(attrs, :name) do
"" -> infer_mod_name_from_git_url(repo)
name -> name
end
path = "#{:ext_mod.modules_dir()}/sources/ejabberd-contrib\/#{mod_name}"
fetch_and_store_repo_source_if_not_exists(path, repo)
:ext_mod.install(mod_name) # Have to check if overwrites an already present mod
end
defp fetch_and_store_repo_source_if_not_exists(path, repo) do
unless File.exists?(path) do
IO.puts "[info] Fetching: #{repo}"
:os.cmd('git clone #{repo} #{path}')
end
end
defp infer_mod_name_from_git_url(repo),
do: String.split(repo, "/") |> List.last |> String.replace(".git", "")
end

View File

@ -0,0 +1,32 @@
defmodule Ejabberd.Config.EjabberdLogger do
@moduledoc """
Module used to log validation errors given validated modules
given validated modules.
"""
alias Ejabberd.Config.EjabberdModule
@doc """
Given a list of modules validated, in the form of {:ok, mod} or
{:error, mod, errors}, it logs to the user the errors found.
"""
@spec log_errors([EjabberdModule.t]) :: [EjabberdModule.t]
def log_errors(modules_validated) when is_list(modules_validated) do
Enum.each modules_validated, &do_log_errors/1
modules_validated
end
defp do_log_errors({:ok, _mod}), do: nil
defp do_log_errors({:error, _mod, errors}), do: Enum.each errors, &do_log_errors/1
defp do_log_errors({:attribute, errors}), do: Enum.each errors, &log_attribute_error/1
defp do_log_errors({:dependency, errors}), do: Enum.each errors, &log_dependency_error/1
defp log_attribute_error({{attr_name, val}, :attr_not_supported}), do:
IO.puts "[ WARN ] Annotation @#{attr_name} is not supported."
defp log_attribute_error({{attr_name, val}, :type_not_supported}), do:
IO.puts "[ WARN ] Annotation @#{attr_name} with value #{inspect val} is not supported (type mismatch)."
defp log_dependency_error({module, :not_found}), do:
IO.puts "[ WARN ] Module #{inspect module} was not found, but is required as a dependency."
end

View File

@ -0,0 +1,46 @@
defmodule Ejabberd.Config.OptsFormatter do
@moduledoc """
Module for formatting options parsed into the format
ejabberd uses.
"""
alias Ejabberd.Config.EjabberdModule
@doc """
Takes a keyword list with keys corresponding to
the keys requested by the ejabberd config (ex: modules: mods)
and formats them to be correctly evaluated by ejabberd.
Look at how Config.get_ejabberd_opts/0 is constructed for
more informations.
"""
@spec format_opts_for_ejabberd([{atom(), any()}]) :: list()
def format_opts_for_ejabberd(opts) do
opts
|> format_attrs_for_ejabberd
end
defp format_attrs_for_ejabberd(opts) when is_list(opts),
do: Enum.map opts, &format_attrs_for_ejabberd/1
defp format_attrs_for_ejabberd({:listeners, mods}),
do: {:listen, format_listeners_for_ejabberd(mods)}
defp format_attrs_for_ejabberd({:modules, mods}),
do: {:modules, format_mods_for_ejabberd(mods)}
defp format_attrs_for_ejabberd({key, opts}) when is_atom(key),
do: {key, opts}
defp format_mods_for_ejabberd(mods) do
Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
{mod, attrs[:opts]}
end
end
defp format_listeners_for_ejabberd(mods) do
Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
Keyword.put(attrs[:opts], :module, mod)
end
end
end

View File

@ -0,0 +1,55 @@
defmodule Ejabberd.Config.Store do
@moduledoc """
Module used for storing the modules parsed from
the configuration file.
Example:
- Store.put(:modules, mod1)
- Store.put(:modules, mod2)
- Store.get(:modules) :: [mod1, mod2]
Be carefoul: when retrieving data you get them
in the order inserted into the store, which normally
is the reversed order of how the modules are specified
inside the configuration file. To resolve this just use
a Enum.reverse/1.
"""
@name __MODULE__
def start_link do
Agent.start_link(fn -> %{} end, name: @name)
end
@doc """
Stores a value based on the key. If the key already exists,
then it inserts the new element, maintaining all the others.
It uses a list for this.
"""
@spec put(atom, any) :: :ok
def put(key, val) do
Agent.update @name, &Map.update(&1, key, [val], fn coll ->
[val | coll]
end)
end
@doc """
Gets a value based on the key passed.
Returns always a list.
"""
@spec get(atom) :: [any]
def get(key) do
Agent.get @name, &Map.get(&1, key, [])
end
@doc """
Stops the store.
It uses Agent.stop underneath, so be aware that exit
could be called.
"""
@spec stop() :: :ok
def stop do
Agent.stop @name
end
end

View File

@ -0,0 +1,40 @@
defmodule Ejabberd.Config.Validation do
@moduledoc """
Module used to validate a list of modules.
"""
@type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
@type mod_validation_result :: {:ok, EjabberdModule.t} | {:error, EjabberdModule.t, map}
alias Ejabberd.Config.EjabberdModule
alias Ejabberd.Config.Attr
alias Ejabberd.Config.Validator
alias Ejabberd.Config.ValidatorUtility
@doc """
Given a module or a list of modules it runs validators on them
and returns {:ok, mod} or {:error, mod, errors}, for each
of them.
"""
@spec validate([EjabberdModule.t] | EjabberdModule.t) :: [mod_validation_result]
def validate(modules) when is_list(modules), do: Enum.map(modules, &do_validate(modules, &1))
def validate(module), do: validate([module])
# Private API
@spec do_validate([EjabberdModule.t], EjabberdModule.t) :: mod_validation_result
defp do_validate(modules, mod) do
{modules, mod, %{}}
|> Validator.Attrs.validate
|> Validator.Dependencies.validate
|> resolve_validation_result
end
@spec resolve_validation_result(mod_validation) :: mod_validation_result
defp resolve_validation_result({_modules, mod, errors}) do
case errors do
err when err == %{} -> {:ok, mod}
err -> {:error, mod, err}
end
end
end

View File

@ -0,0 +1,28 @@
defmodule Ejabberd.Config.Validator.Attrs do
@moduledoc """
Validator module used to validate attributes.
"""
# TODO: Duplicated from validator.ex !!!
@type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
import Ejabberd.Config.ValidatorUtility
alias Ejabberd.Config.Attr
@doc """
Given a module (with the form used for validation)
it runs Attr.validate/1 on each attribute and
returns the validation tuple with the errors updated, if found.
"""
@spec validate(mod_validation) :: mod_validation
def validate({modules, mod, errors}) do
errors = Enum.reduce mod.attrs, errors, fn(attr, err) ->
case Attr.validate(attr) do
{:ok, attr} -> err
{:error, attr, cause} -> put_error(err, :attribute, {attr, cause})
end
end
{modules, mod, errors}
end
end

View File

@ -0,0 +1,30 @@
defmodule Ejabberd.Config.Validator.Dependencies do
@moduledoc """
Validator module used to validate dependencies specified
with the @dependency annotation.
"""
# TODO: Duplicated from validator.ex !!!
@type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
import Ejabberd.Config.ValidatorUtility
@doc """
Given a module (with the form used for validation)
it checks if the @dependency annotation is respected and
returns the validation tuple with the errors updated, if found.
"""
@spec validate(mod_validation) :: mod_validation
def validate({modules, mod, errors}) do
module_names = extract_module_names(modules)
dependencies = mod.attrs[:dependency]
errors = Enum.reduce dependencies, errors, fn(req_module, err) ->
case req_module in module_names do
true -> err
false -> put_error(err, :dependency, {req_module, :not_found})
end
end
{modules, mod, errors}
end
end

View File

@ -0,0 +1,30 @@
defmodule Ejabberd.Config.ValidatorUtility do
@moduledoc """
Module used as a base validator for validation modules.
Imports utility functions for working with validation structures.
"""
alias Ejabberd.Config.EjabberdModule
@doc """
Inserts an error inside the errors collection, for the given key.
If the key doesn't exists then it creates an empty collection
and inserts the value passed.
"""
@spec put_error(map, atom, any) :: map
def put_error(errors, key, val) do
Map.update errors, key, [val], fn coll ->
[val | coll]
end
end
@doc """
Given a list of modules it extracts and returns a list
of the module names (which are Elixir.Module).
"""
@spec extract_module_names(EjabberdModule.t) :: [atom]
def extract_module_names(modules) when is_list(modules) do
modules
|> Enum.map(&Map.get(&1, :module))
end
end

View File

@ -0,0 +1,18 @@
defmodule Ejabberd.ConfigUtil do
@moduledoc """
Module containing utility functions for
the config file.
"""
@doc """
Returns true when the config file is based on elixir.
"""
@spec is_elixir_config(list) :: boolean
def is_elixir_config(filename) when is_list(filename) do
is_elixir_config(to_string(filename))
end
def is_elixir_config(filename) do
String.ends_with?(filename, "exs")
end
end

19
lib/ejabberd/module.ex Normal file
View File

@ -0,0 +1,19 @@
defmodule Ejabberd.Module do
defmacro __using__(opts) do
logger_enabled = Keyword.get(opts, :logger, true)
quote do
@behaviour :gen_mod
import Ejabberd.Module
unquote(if logger_enabled do
quote do: import Ejabberd.Logger
end)
end
end
# gen_mod callbacks
def depends(_host, _opts), do: []
def mod_opt_type(_), do: []
end

View File

@ -0,0 +1,94 @@
defmodule Mix.Tasks.Ejabberd.Deps.Tree do
use Mix.Task
alias Ejabberd.Config.EjabberdModule
@shortdoc "Lists all ejabberd modules and their dependencies"
@moduledoc """
Lists all ejabberd modules and their dependencies.
The project must have ejabberd as a dependency.
"""
def run(_argv) do
# First we need to start manually the store to be available
# during the compilation of the config file.
Ejabberd.Config.Store.start_link
Ejabberd.Config.init(:ejabberd_config.get_ejabberd_config_path())
Mix.shell.info "ejabberd modules"
Ejabberd.Config.Store.get(:modules)
|> Enum.reverse # Because of how mods are stored inside the store
|> format_mods
|> Mix.shell.info
end
defp format_mods(mods) when is_list(mods) do
deps_tree = build_dependency_tree(mods)
mods_used_as_dependency = get_mods_used_as_dependency(deps_tree)
keep_only_mods_not_used_as_dep(deps_tree, mods_used_as_dependency)
|> format_mods_into_string
end
defp build_dependency_tree(mods) do
Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
deps = attrs[:dependency]
build_dependency_tree(mods, mod, deps)
end
end
defp build_dependency_tree(mods, mod, []), do: %{module: mod, dependency: []}
defp build_dependency_tree(mods, mod, deps) when is_list(deps) do
dependencies = Enum.map deps, fn dep ->
dep_deps = get_dependencies_of_mod(mods, dep)
build_dependency_tree(mods, dep, dep_deps)
end
%{module: mod, dependency: dependencies}
end
defp get_mods_used_as_dependency(mods) when is_list(mods) do
Enum.reduce mods, [], fn(mod, acc) ->
case mod do
%{dependency: []} -> acc
%{dependency: deps} -> get_mod_names(deps) ++ acc
end
end
end
defp get_mod_names([]), do: []
defp get_mod_names(mods) when is_list(mods), do: Enum.map(mods, &get_mod_names/1) |> List.flatten
defp get_mod_names(%{module: mod, dependency: deps}), do: [mod | get_mod_names(deps)]
defp keep_only_mods_not_used_as_dep(mods, mods_used_as_dep) do
Enum.filter mods, fn %{module: mod} ->
not mod in mods_used_as_dep
end
end
defp get_dependencies_of_mod(deps, mod_name) do
Enum.find(deps, &(Map.get(&1, :module) == mod_name))
|> Map.get(:attrs)
|> Keyword.get(:dependency)
end
defp format_mods_into_string(mods), do: format_mods_into_string(mods, 0)
defp format_mods_into_string([], _indentation), do: ""
defp format_mods_into_string(mods, indentation) when is_list(mods) do
Enum.reduce mods, "", fn(mod, acc) ->
acc <> format_mods_into_string(mod, indentation)
end
end
defp format_mods_into_string(%{module: mod, dependency: deps}, 0) do
"\n├── #{mod}" <> format_mods_into_string(deps, 2)
end
defp format_mods_into_string(%{module: mod, dependency: deps}, indentation) do
spaces = Enum.reduce 0..indentation, "", fn(_, acc) -> " " <> acc end
"\n#{spaces}└── #{mod}" <> format_mods_into_string(deps, indentation + 4)
end
end

View File

@ -1,16 +1,15 @@
defmodule ModPresenceDemo do
import Ejabberd.Logger # this allow using info, error, etc for logging
@behaviour :gen_mod
use Ejabberd.Module
def start(host, _opts) do
info('Starting ejabberd module Presence Demo')
Ejabberd.Hooks.add(:set_presence_hook, host, __ENV__.module, :on_presence, 50)
Ejabberd.Hooks.add(:set_presence_hook, host, __MODULE__, :on_presence, 50)
:ok
end
def stop(host) do
info('Stopping ejabberd module Presence Demo')
Ejabberd.Hooks.delete(:set_presence_hook, host, __ENV__.module, :on_presence, 50)
Ejabberd.Hooks.delete(:set_presence_hook, host, __MODULE__, :on_presence, 50)
:ok
end

69
mix.exs
View File

@ -3,7 +3,7 @@ defmodule Ejabberd.Mixfile do
def project do
[app: :ejabberd,
version: "16.06.0",
version: "16.11.0",
description: description,
elixir: "~> 1.2",
elixirc_paths: ["lib"],
@ -11,11 +11,13 @@ defmodule Ejabberd.Mixfile do
compilers: [:asn1] ++ Mix.compilers,
erlc_options: erlc_options,
erlc_paths: ["asn1", "src"],
# Elixir tests are starting the part of ejabberd they need
aliases: [test: "test --no-start"],
package: package,
deps: deps]
end
defp description do
def description do
"""
Robust, ubiquitous and massively scalable Jabber / XMPP Instant Messaging platform.
"""
@ -26,9 +28,8 @@ defmodule Ejabberd.Mixfile do
applications: [:ssl],
included_applications: [:lager, :mnesia, :p1_utils, :cache_tab,
:fast_tls, :stringprep, :fast_xml,
:stun, :fast_yaml, :ezlib, :iconv,
:esip, :jiffy, :p1_oauth2, :p1_xmlrpc, :eredis,
:p1_mysql, :p1_pgsql, :sqlite3]]
:stun, :fast_yaml, :esip, :jiffy, :p1_oauth2]
++ cond_apps]
end
defp erlc_options do
@ -38,7 +39,7 @@ defmodule Ejabberd.Mixfile do
end
defp deps do
[{:lager, "~> 3.0.0"},
[{:lager, "~> 3.2"},
{:p1_utils, "~> 1.0"},
{:cache_tab, "~> 1.0"},
{:stringprep, "~> 1.0"},
@ -49,17 +50,40 @@ defmodule Ejabberd.Mixfile do
{:esip, "~> 1.0"},
{:jiffy, "~> 0.14.7"},
{:p1_oauth2, "~> 0.6.1"},
{:p1_xmlrpc, "~> 1.15"},
{:p1_mysql, "~> 1.0"},
{:p1_pgsql, "~> 1.1"},
{:sqlite3, "~> 1.1"},
{:ezlib, "~> 1.0"},
{:iconv, "~> 1.0"},
{:eredis, "~> 1.0"},
{:exrm, "~> 1.0.0-rc7", only: :dev}]
{:exrm, "~> 1.0.0", only: :dev},
# relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with
# version 3.20:
{:relx, "~> 3.21", only: :dev},
{:ex_doc, ">= 0.0.0", only: :dev}]
++ cond_deps
end
defp package do
defp cond_deps do
for {:true, dep} <- [{config(:mysql), {:p1_mysql, "~> 1.0"}},
{config(:pgsql), {:p1_pgsql, "~> 1.1"}},
{config(:sqlite), {:sqlite3, "~> 1.1"}},
{config(:riak), {:riakc, "~> 2.4"}},
{config(:redis), {:eredis, "~> 1.0"}},
{config(:zlib), {:ezlib, "~> 1.0"}},
{config(:iconv), {:iconv, "~> 1.0"}},
{config(:pam), {:p1_pam, "~> 1.0"}},
{config(:tools), {:luerl, github: "rvirding/luerl", tag: "v0.2"}},
{config(:tools), {:meck, "~> 0.8.4"}},
{config(:tools), {:moka, github: "processone/moka", tag: "1.0.5c"}}], do:
dep
end
defp cond_apps do
for {:true, app} <- [{config(:redis), :eredis},
{config(:mysql), :p1_mysql},
{config(:pgsql), :p1_pgsql},
{config(:sqlite), :sqlite3},
{config(:zlib), :ezlib},
{config(:iconv), :iconv}], do:
app
end
def package do
[# These are the default files included in the package
files: ["lib", "src", "priv", "mix.exs", "include", "README.md", "COPYING"],
maintainers: ["ProcessOne"],
@ -69,6 +93,21 @@ defmodule Ejabberd.Mixfile do
"Source" => "https://github.com/processone/ejabberd",
"ProcessOne" => "http://www.process-one.net/"}]
end
def vars do
case :file.consult("vars.config") do
{:ok,config} -> config
_ -> [zlib: true, iconv: true]
end
end
defp config(key) do
case vars[key] do
nil -> false
value -> value
end
end
end
defmodule Mix.Tasks.Compile.Asn1 do

View File

@ -1,26 +1,23 @@
%{"bbmustache": {:hex, :bbmustache, "1.0.4", "7ba94f971c5afd7b6617918a4bb74705e36cab36eb84b19b6a1b7ee06427aa38", [:rebar], []},
"cache_tab": {:hex, :cache_tab, "1.0.3", "0e3c40dde2fe2a6a4db241d7583cea0cc1bcf29e546a0a22f15b75366b2f336e", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
"cache_tab": {:hex, :cache_tab, "1.0.4", "3fd2b1ab40c36e7830a4e09e836c6b0fa89191cd4e5fd471873e4eb42f5cd37c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []},
"eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []},
"earmark": {:hex, :earmark, "1.0.2", "a0b0904d74ecc14da8bd2e6e0248e1a409a2bc91aade75fcf428125603de3853", [:mix], []},
"erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]},
"esip": {:hex, :esip, "1.0.6", "cb1ced88fae4c4a4888d9023c2c13b2239e14f8e360aee134c964b4a36dcc34d", [:rebar3], [{:stun, "1.0.5", [hex: :stun, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.5", [hex: :fast_tls, optional: false]}]},
"exrm": {:hex, :exrm, "1.0.6", "f708fc091dcacb93c1da58254a1ab34166d5ac3dca162877e878fe5d7a9e9dce", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]},
"esip": {:hex, :esip, "1.0.8", "69885a6c07964aabc6c077fe1372aa810a848bd3d9a415b160dabdce9c7a79b5", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}, {:stun, "1.0.7", [hex: :stun, optional: false]}]},
"ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]},
"exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]},
"ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []},
"fast_tls": {:hex, :fast_tls, "1.0.5", "8b970a91d4131fe5b9d47ffaccc2466944293c88dc5cc75a25548d73d57f7b77", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
"fast_xml": {:hex, :fast_xml, "1.1.13", "85eca0a003598dbb0644320bd9bdc5fef30ad6285ab2aa80e2b5b82e65b79aa8", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
"fast_yaml": {:hex, :fast_yaml, "1.0.4", "075ffb55f6ff3aa2f0461b8bfd1218e2f91e632c1675fc535963b9de7834800e", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
"fast_tls": {:hex, :fast_tls, "1.0.7", "9b72ecfcdcad195ab072c196fab8334f49d8fea76bf1a51f536d69e7527d902a", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"fast_xml": {:hex, :fast_xml, "1.1.15", "6d23eb7f874e1357cf80a48d75a7bd0c8f6318029dc4b70122e9f54911f57f83", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"fast_yaml": {:hex, :fast_yaml, "1.0.6", "3fe6feb7935ae8028b337e53e1db29e73ad3bca8041108f6a8f73b7175ece75c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"getopt": {:hex, :getopt, "0.8.2", "b17556db683000ba50370b16c0619df1337e7af7ecbf7d64fbf8d1d6bce3109b", [:rebar], []},
"goldrush": {:hex, :goldrush, "0.1.7", "349a351d17c71c2fdaa18a6c2697562abe136fec945f147b381f0cf313160228", [:rebar3], []},
"iconv": {:hex, :iconv, "1.0.0", "5ff1c54e5b3b9a8235de872632e9612c7952acdf89bc21db2f2efae0e72647be", [:rebar3], []},
"goldrush": {:hex, :goldrush, "0.1.8", "2024ba375ceea47e27ea70e14d2c483b2d8610101b4e852ef7f89163cdb6e649", [:rebar3], []},
"iconv": {:hex, :iconv, "1.0.2", "a0792f06ab4b5ea1b5bb49789405739f1281a91c44cf3879cb70e4d777666217", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []},
"lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]},
"p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []},
"lager": {:hex, :lager, "3.2.1", "eef4e18b39e4195d37606d9088ea05bf1b745986cf8ec84f01d332456fe88d17", [:rebar3], [{:goldrush, "0.1.8", [hex: :goldrush, optional: false]}]},
"p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []},
"p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []},
"p1_utils": {:hex, :p1_utils, "1.0.4", "7face65db102b5d1ebe7ad3c7517c5ee8cfbe174c6658e3affbb00eb66e06787", [:rebar3], []},
"p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []},
"p1_utils": {:hex, :p1_utils, "1.0.5", "3e698354fdc1fea5491d991457b0cb986c0a00a47d224feb841dc3ec82b9f721", [:rebar3], []},
"providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]},
"relx": {:hex, :relx, "3.20.0", "b515b8317d25b3a1508699294c3d1fa6dc0527851dffc87446661bce21a36710", [:rebar3], [{:providers, "1.6.0", [hex: :providers, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}]},
"sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []},
"stringprep": {:hex, :stringprep, "1.0.4", "f8f94d838ed202787699ff71d67b65481d350bda32b232ba1db52faca8eaed39", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
"stun": {:hex, :stun, "1.0.5", "ec1d9928f25451d6fd2d2ade58c46b58b8d2c8326ddea3a667e926d04792f82c", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.5", [hex: :fast_tls, optional: false]}]}}
"relx": {:hex, :relx, "3.21.1", "f989dc520730efd9075e9f4debcb8ba1d7d1e86b018b0bcf45a2eb80270b4ad6", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]},
"stringprep": {:hex, :stringprep, "1.0.6", "1cf1c439eb038aa590da5456e019f86afbfbfeb5a2d37b6e5f873041624c6701", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"stun": {:hex, :stun, "1.0.7", "904dc6f26a3c30c54881c4c3003699f2a4968067ee6b3aecdf9895aad02df75e", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}}

View File

@ -144,7 +144,7 @@
{"Import Users from Dir at ","Importovat uživatele z adresáře na "}.
{"Import Users From jabberd14 Spool Files","Importovat uživatele z jabberd14 spool souborů"}.
{"Improper message type","Nesprávný typ zprávy"}.
{"Incoming s2s Connections:",""}.
{"Incoming s2s Connections:","Příchozí s2s spojení:"}.
{"Incorrect password","Nesprávné heslo"}.
{"Invalid affiliation: ~s","Neplatné přiřazení: ~s"}.
{"Invalid role: ~s","Neplatná role: ~s"}.
@ -333,7 +333,6 @@
{"September",". září"}.
{"Server ~b","Server ~b"}.
{"Server:","Server:"}.
{"Server","Server"}.
{"Set message of the day and send to online users","Nastavit zprávu dne a odeslat ji online uživatelům"}.
{"Set message of the day on all hosts and send to online users","Nastavit zprávu dne a odeslat ji online uživatelům"}.
{"Shared Roster Groups","Skupiny pro sdílený seznam kontaktů"}.
@ -409,10 +408,10 @@
{"User JID","Jabber ID uživatele"}.
{"User Management","Správa uživatelů"}.
{"Username:","Uživatelské jméno:"}.
{"User ~s",""}.
{"Users are not allowed to register accounts so quickly","Je zakázáno registrovat účty v tak rychlém sledu"}.
{"Users Last Activity","Poslední aktivita uživatele"}.
{"Users","Uživatelé"}.
{"User ~s","Uživatel ~s"}.
{"User","Uživatel"}.
{"Validate","Ověřit"}.
{"vCard User Search","Hledání uživatelů podle vizitek"}.

View File

@ -242,9 +242,7 @@ msgstr "Odchozí s2s spojení:"
#: ejabberd_web_admin.erl:1559
msgid "Incoming s2s Connections:"
msgstr ""
"Příchozí\n"
" s2s spojení:"
msgstr "Příchozí s2s spojení:"
#: ejabberd_web_admin.erl:1595 ejabberd_web_admin.erl:1794
#: ejabberd_web_admin.erl:1804 ejabberd_web_admin.erl:2214 mod_roster.erl:1429
@ -258,9 +256,7 @@ msgstr "Změnit heslo"
#: ejabberd_web_admin.erl:1673
msgid "User ~s"
msgstr ""
"Uživatel\n"
" ~s"
msgstr "Uživatel ~s"
#: ejabberd_web_admin.erl:1684
msgid "Connected Resources:"

View File

@ -9,16 +9,15 @@
{deps, [{lager, ".*", {git, "https://github.com/basho/lager", {tag, "3.2.1"}}},
{p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.5"}}},
{cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.3"}}},
{fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.6"}}},
{stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.5"}}},
{fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.14"}}},
{stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.6"}}},
{esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.7"}}},
{fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.5"}}},
{cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.4"}}},
{fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.7"}}},
{stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.6"}}},
{fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.15"}}},
{stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.7"}}},
{esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.8"}}},
{fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.6"}}},
{jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.7"}}},
{p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.1"}}},
{p1_xmlrpc, ".*", {git, "https://github.com/processone/p1_xmlrpc", {tag, "1.15.1"}}},
{luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.2"}}},
{if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql",
{tag, "1.0.1"}}}},
@ -34,16 +33,16 @@
{tag, "2.4.1"}}}},
%% Elixir support, needed to run tests
{if_var_true, elixir, {elixir, ".*", {git, "https://github.com/elixir-lang/elixir",
{tag, "v1.1.1"}}}},
{tag, {if_version_above, "17", "v1.2.6", "v1.1.1"}}}}},
%% TODO: When modules are fully migrated to new structure and mix, we will not need anymore rebar_elixir_plugin
{if_var_true, elixir, {rebar_elixir_plugin, ".*",
{git, "https://github.com/processone/rebar_elixir_plugin", "0.1.0"}}},
{if_var_true, iconv, {iconv, ".*", {git, "https://github.com/processone/iconv",
{tag, "1.0.1"}}}},
{tag, "1.0.2"}}}},
{if_var_true, tools, {meck, "0.8.*", {git, "https://github.com/eproxus/meck",
{tag, "0.8.4"}}}},
{if_var_true, tools, {moka, ".*", {git, "https://github.com/processone/moka.git",
{tag, "1.0.5b"}}}},
{tag, "1.0.5c"}}}},
{if_var_true, redis, {eredis, ".*", {git, "https://github.com/wooga/eredis",
{tag, "v1.0.8"}}}}]}.
@ -70,7 +69,9 @@
{if_var_true, debug, debug_info},
{if_var_true, roster_gateway_workaround, {d, 'ROSTER_GATWAY_WORKAROUND'}},
{if_var_match, db_type, mssql, {d, 'mssql'}},
{if_var_true, elixir, {d, 'ELIXIR_ENABLED'}},
{if_var_true, erlang_deprecated_types, {d, 'ERL_DEPRECATED_TYPES'}},
{if_version_above, "18", {d, 'STRONG_RAND_BYTES'}},
{if_var_true, hipe, native},
{src_dirs, [asn1, src,
{if_var_true, tools, tools},

View File

@ -19,7 +19,7 @@ ModCfg0 = fun(F, Cfg, [Key|Tail], Op, Default) ->
[{Key, F(F, OldVal, Tail, Op, Default)} | PartCfg]
end
end,
ModCfg = fun(Cfg, Keys, Op, Default) -> ModCfg0(ModCfg0, Cfg, Keys, Op, Default) end.
ModCfg = fun(Cfg, Keys, Op, Default) -> ModCfg0(ModCfg0, Cfg, Keys, Op, Default) end,
Cfg = case file:consult(filename:join(filename:dirname(SCRIPT), "vars.config")) of
{ok, Terms} ->
@ -28,6 +28,13 @@ Cfg = case file:consult(filename:join(filename:dirname(SCRIPT), "vars.config"))
[]
end,
ProcessSingleVar = fun(F, Var, Tail) ->
case F(F, [Var], []) of
[] -> Tail;
[Val] -> [Val | Tail]
end
end,
ProcessVars = fun(_F, [], Acc) ->
lists:reverse(Acc);
(F, [{Type, Ver, Value} | Tail], Acc) when
@ -40,17 +47,31 @@ ProcessVars = fun(_F, [], Acc) ->
SysVer < Ver
end,
if Include ->
F(F, Tail, [Value | Acc]);
F(F, Tail, ProcessSingleVar(F, Value, Acc));
true ->
F(F, Tail, Acc)
end;
(F, [{Type, Ver, Value, ElseValue} | Tail], Acc) when
Type == if_version_above orelse
Type == if_version_below ->
SysVer = erlang:system_info(otp_release),
Include = if Type == if_version_above ->
SysVer > Ver;
true ->
SysVer < Ver
end,
if Include ->
F(F, Tail, ProcessSingleVar(F, Value, Acc));
true ->
F(F, Tail, ProcessSingleVar(F, ElseValue, Acc))
end;
(F, [{Type, Var, Value} | Tail], Acc) when
Type == if_var_true orelse
Type == if_var_false ->
Flag = Type == if_var_true,
case proplists:get_bool(Var, Cfg) of
V when V == Flag ->
F(F, Tail, [Value | Acc]);
F(F, Tail, ProcessSingleVar(F, Value, Acc));
_ ->
F(F, Tail, Acc)
end;
@ -59,7 +80,7 @@ ProcessVars = fun(_F, [], Acc) ->
Type == if_var_no_match ->
case proplists:get_value(Var, Cfg) of
V when V == Match ->
F(F, Tail, [Value | Acc]);
F(F, Tail, ProcessSingleVar(F, Value, Acc));
_ ->
F(F, Tail, Acc)
end;
@ -146,7 +167,7 @@ Conf6 = case {lists:keyfind(cover_enabled, 1, Conf5), os:getenv("TRAVIS")} of
Conf5
end,
%io:format("ejabberd configuration:~n ~p~n", [Conf5]),
%io:format("ejabberd configuration:~n ~p~n", [Conf6]),
Conf6.

View File

@ -3387,6 +3387,71 @@
dec = {dec_int, [0, infinity]},
enc = {enc_int, []}}]}).
-xml(privilege_perm,
#elem{name = <<"perm">>,
xmlns = <<"urn:xmpp:privilege:1">>,
result = {privilege_perm, '$access', '$type'},
attrs = [#attr{name = <<"access">>,
required = true,
dec = {dec_enum, [[roster, message, presence]]},
enc = {enc_enum, []}},
#attr{name = <<"type">>,
required = true,
dec = {dec_enum, [[none, get, set, both,
outgoing, roster,
managed_entity]]},
enc = {enc_enum, []}}]}).
-xml(privilege,
#elem{name = <<"privilege">>,
xmlns = <<"urn:xmpp:privilege:1">>,
result = {privilege, '$perms', '$forwarded'},
refs = [#ref{name = privilege_perm, label = '$perms'},
#ref{name = forwarded, min = 0,
max = 1, label = '$forwarded'}]}).
-xml(delegated_attribute,
#elem{name = <<"attribute">>,
xmlns = <<"urn:xmpp:delegation:1">>,
result = '$name',
attrs = [#attr{name = <<"name">>,
required = true}]}).
-xml(delegated,
#elem{name = <<"delegated">>,
xmlns = <<"urn:xmpp:delegation:1">>,
result = {delegated, '$ns', '$attrs'},
attrs = [#attr{name = <<"namespace">>,
label = '$ns',
required = true}],
refs = [#ref{name = delegated_attribute,
label = '$attrs'}]}).
-xml(delegation,
#elem{name = <<"delegation">>,
xmlns = <<"urn:xmpp:delegation:1">>,
result = {delegation, '$delegated', '$forwarded'},
refs = [#ref{name = delegated, label = '$delegated'},
#ref{name = forwarded, min = 0,
max = 1, label = '$forwarded'}]}).
-xml(delegate,
#elem{name = <<"delegate">>,
xmlns = <<"urn:xmpp:delegation:1">>,
result = '$namespace',
attrs = [#attr{name = <<"namespace">>,
required = true}]}).
-xml(delegation_query,
#elem{name = <<"query">>,
xmlns = <<"urn:xmpp:delegation:1">>,
result = {delegation_query, '$to', '$delegate'},
attrs = [#attr{name = <<"to">>,
required = true,
dec = {dec_jid, []},
enc = {enc_jid, []}}],
refs = [#ref{name = delegate, label = '$delegate'}]}).
-spec dec_tzo(_) -> {integer(), integer()}.
dec_tzo(Val) ->
[H1, M1] = str:tokens(Val, <<":">>),

View File

@ -313,3 +313,10 @@ CREATE TABLE sm (
CREATE UNIQUE INDEX i_sm_sid ON sm(usec, pid);
CREATE INDEX i_sm_node ON sm(node);
CREATE INDEX i_sm_username ON sm(username);
CREATE TABLE oauth_token (
token text NOT NULL PRIMARY KEY,
jid text NOT NULL,
scope text NOT NULL,
expire bigint NOT NULL
);

View File

@ -480,3 +480,13 @@ ON DELETE CASCADE;
ALTER TABLE [dbo].[pubsub_state] CHECK CONSTRAINT [pubsub_state_ibfk_1];
CREATE TABLE [dbo].[oauth_token] (
[token] [varchar] (250) NOT NULL,
[jid] [text] NOT NULL,
[scope] [text] NOT NULL,
[expire] [bigint] NOT NULL,
CONSTRAINT [oauth_token_PRIMARY] PRIMARY KEY CLUSTERED
(
[token] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
) TEXTIMAGE_ON [PRIMARY];

View File

@ -328,3 +328,10 @@ CREATE TABLE sm (
CREATE UNIQUE INDEX i_sid ON sm(usec, pid(75));
CREATE INDEX i_node ON sm(node(75));
CREATE INDEX i_username ON sm(username);
CREATE TABLE oauth_token (
token varchar(191) NOT NULL PRIMARY KEY,
jid text NOT NULL,
scope text NOT NULL,
expire bigint NOT NULL
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -330,3 +330,12 @@ CREATE TABLE sm (
CREATE UNIQUE INDEX i_sm_sid ON sm USING btree (usec, pid);
CREATE INDEX i_sm_node ON sm USING btree (node);
CREATE INDEX i_sm_username ON sm USING btree (username);
CREATE TABLE oauth_token (
token text NOT NULL,
jid text NOT NULL,
scope text NOT NULL,
expire bigint NOT NULL
);
CREATE UNIQUE INDEX i_oauth_token_token ON oauth_token USING btree (token);

View File

@ -31,11 +31,13 @@
-export([add_access/3, clear/0]).
-export([start/0, add/3, add_list/3, add_local/3, add_list_local/3,
load_from_config/0, match_rule/3,
load_from_config/0, match_rule/3, any_rules_allowed/3,
transform_options/1, opt_type/1, acl_rule_matches/3,
acl_rule_verify/1, access_matches/3,
transform_access_rules_config/1,
access_rules_validator/1, shaper_rules_validator/1]).
parse_ip_netmask/1,
access_rules_validator/1, shaper_rules_validator/1,
normalize_spec/1, resolve_access/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@ -74,12 +76,6 @@
-export_type([acl/0]).
start() ->
case catch mnesia:table_info(acl, storage_type) of
disc_copies ->
mnesia:delete_table(acl);
_ ->
ok
end,
mnesia:create_table(acl,
[{ram_copies, [node()]}, {type, bag},
{local_content, true},
@ -261,6 +257,7 @@ normalize_spec(Spec) ->
{server, S} -> {server, nameprep(S)};
{resource, R} -> {resource, resourceprep(R)};
{server_regexp, SR} -> {server_regexp, b(SR)};
{resource_regexp, R} -> {resource_regexp, b(R)};
{server_glob, S} -> {server_glob, b(S)};
{resource_glob, R} -> {resource_glob, b(R)};
{ip, {Net, Mask}} -> {ip, {Net, Mask}};
@ -274,6 +271,15 @@ normalize_spec(Spec) ->
end
end.
-spec any_rules_allowed(global | binary(), access_name(),
jid() | ljid() | inet:ip_address()) -> boolean().
any_rules_allowed(Host, Access, Entity) ->
lists:any(fun (Rule) ->
allow == acl:match_rule(Host, Rule, Entity)
end,
Access).
-spec match_rule(global | binary(), access_name(),
jid() | ljid() | inet:ip_address()) -> any().
@ -432,30 +438,35 @@ acl_rule_matches({node_glob, {UR, SR}}, #{usr := {U, S, _}}, _Host) ->
acl_rule_matches(_ACL, _Data, _Host) ->
false.
-spec access_matches(atom()|list(), any(), global|binary()) -> any().
access_matches(all, _Data, _Host) ->
allow;
access_matches(none, _Data, _Host) ->
deny;
access_matches(Name, Data, Host) when is_atom(Name) ->
GAccess = ets:lookup(access, {Name, global}),
resolve_access(all, _Host) ->
all;
resolve_access(none, _Host) ->
none;
resolve_access(Name, Host) when is_atom(Name) ->
GAccess = mnesia:dirty_read(access, {Name, global}),
LAccess =
if Host /= global -> ets:lookup(access, {Name, Host});
if Host /= global -> mnesia:dirty_read(access, {Name, Host});
true -> []
end,
case GAccess ++ LAccess of
[] ->
deny;
[];
AccessList ->
Rules = lists:flatmap(
lists:flatmap(
fun(#access{rules = Rs}) ->
Rs
end, AccessList),
access_rules_matches(Rules, Data, Host)
end, AccessList)
end;
access_matches(Rules, Data, Host) when is_list(Rules) ->
access_rules_matches(Rules, Data, Host).
resolve_access(Rules, _Host) when is_list(Rules) ->
Rules.
-spec access_matches(atom()|list(), any(), global|binary()) -> allow|deny.
access_matches(Rules, Data, Host) ->
case resolve_access(Rules, Host) of
all -> allow;
none -> deny;
RRules -> access_rules_matches(RRules, Data, Host)
end.
-spec access_rules_matches(list(), any(), global|binary()) -> any().
@ -473,7 +484,7 @@ access_rules_matches([], _Data, _Host, Default) ->
Default.
get_aclspecs(ACL, Host) ->
ets:lookup(acl, {ACL, Host}) ++ ets:lookup(acl, {ACL, global}).
mnesia:dirty_read(acl, {ACL, Host}) ++ mnesia:dirty_read(acl, {ACL, global}).
is_regexp_match(String, RegExp) ->
case ejabberd_regexp:run(String, RegExp) of
@ -676,7 +687,8 @@ transform_options({acl, Name, Type}, Opts) ->
{server_regexp, SR} -> {server_regexp, [b(SR)]};
{server_glob, S} -> {server_glob, [b(S)]};
{ip, S} -> {ip, [b(S)]};
{resource_glob, R} -> {resource_glob, [b(R)]}
{resource_glob, R} -> {resource_glob, [b(R)]};
{resource_regexp, R} -> {resource_regexp, [b(R)]}
end,
[{acl, [{Name, [T]}]}|Opts];
transform_options({access, Name, Rules}, Opts) ->

View File

@ -51,7 +51,7 @@ mech_step(State, ClientIn) ->
{ok,
[{username, User}, {authzid, AuthzId},
{auth_module, ejabberd_oauth}]};
false ->
_ ->
{error, 'not-authorized', User}
end;
_ -> {error, 'bad-protocol'}

View File

@ -85,7 +85,7 @@ mech_step(#state{step = 2} = State, ClientIn) ->
if is_tuple(Ret) -> Ret;
true ->
TempSalt =
crypto:rand_bytes(?SALT_LENGTH),
randoms:bytes(?SALT_LENGTH),
SaltedPassword =
scram:salted_password(Ret,
TempSalt,
@ -99,7 +99,7 @@ mech_step(#state{step = 2} = State, ClientIn) ->
str:substr(ClientIn,
str:str(ClientIn, <<"n=">>)),
ServerNonce =
jlib:encode_base64(crypto:rand_bytes(?NONCE_LENGTH)),
jlib:encode_base64(randoms:bytes(?NONCE_LENGTH)),
ServerFirstMessage =
iolist_to_binary(
["r=",

View File

@ -105,8 +105,6 @@ start_app([], _Type, _StartFlag) ->
ok.
check_app_modules(App, StartFlag) ->
{A, B, C} = p1_time_compat:timestamp(),
random:seed(A, B, C),
sleep(5000),
case application:get_key(App, modules) of
{ok, Mods} ->
@ -140,7 +138,7 @@ exit_or_halt(Reason, StartFlag) ->
end.
sleep(N) ->
timer:sleep(random:uniform(N)).
timer:sleep(randoms:uniform(N)).
get_module_file(App, Mod) ->
BaseName = atom_to_list(Mod),

View File

@ -0,0 +1,543 @@
%%%-------------------------------------------------------------------
%%% File : ejabberd_access_permissions.erl
%%% Author : Paweł Chmielowski <pawel@process-one.net>
%%% Purpose : Administrative functions and commands
%%% Created : 7 Sep 2016 by Paweł Chmielowski <pawel@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%-------------------------------------------------------------------
-module(ejabberd_access_permissions).
-author("pawel@process-one.net").
-include("ejabberd_commands.hrl").
-include("logger.hrl").
-behaviour(gen_server).
-behavior(ejabberd_config).
%% API
-export([start_link/0,
parse_api_permissions/1,
can_access/2,
invalidate/0,
opt_type/1,
show_current_definitions/0,
register_permission_addon/2,
unregister_permission_addon/1]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-define(SERVER, ?MODULE).
-record(state, {
definitions = none,
fragments_generators = []
}).
%%%===================================================================
%%% API
%%%===================================================================
-spec can_access(atom(), map()) -> allow | deny.
can_access(Cmd, CallerInfo) ->
gen_server:call(?MODULE, {can_access, Cmd, CallerInfo}).
-spec invalidate() -> ok.
invalidate() ->
gen_server:cast(?MODULE, invalidate).
-spec register_permission_addon(atom(), fun()) -> ok.
register_permission_addon(Name, Fun) ->
gen_server:call(?MODULE, {register_config_fragment_generator, Name, Fun}).
-spec unregister_permission_addon(atom()) -> ok.
unregister_permission_addon(Name) ->
gen_server:call(?MODULE, {unregister_config_fragment_generator, Name}).
-spec show_current_definitions() -> any().
show_current_definitions() ->
gen_server:call(?MODULE, show_current_definitions).
%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%%
%% @end
%%--------------------------------------------------------------------
-spec start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% @end
%%--------------------------------------------------------------------
-spec init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore.
init([]) ->
{ok, #state{}}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @end
%%--------------------------------------------------------------------
-spec handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}.
handle_call({can_access, Cmd, CallerInfo}, _From, State) ->
CallerModule = maps:get(caller_module, CallerInfo, none),
Host = maps:get(caller_host, CallerInfo, global),
{State2, Defs0} = get_definitions(State),
Defs = maps:get(extra_permissions, CallerInfo, []) ++ Defs0,
Res = lists:foldl(
fun({Name, _} = Def, none) ->
case matches_definition(Def, Cmd, CallerModule, Host, CallerInfo) of
true ->
?DEBUG("Command '~p' execution allowed by rule '~s' (CallerInfo=~p)", [Cmd, Name, CallerInfo]),
allow;
_ ->
none
end;
(_, Val) ->
Val
end, none, Defs),
Res2 = case Res of
allow -> allow;
_ ->
?DEBUG("Command '~p' execution denied (CallerInfo=~p)", [Cmd, CallerInfo]),
deny
end,
{reply, Res2, State2};
handle_call(show_current_definitions, _From, State) ->
{State2, Defs} = get_definitions(State),
{reply, Defs, State2};
handle_call({register_config_fragment_generator, Name, Fun}, _From, #state{fragments_generators = Gens} = State) ->
NGens = lists:keystore(Name, 1, Gens, {Name, Fun}),
{reply, ok, State#state{fragments_generators = NGens}};
handle_call({unregister_config_fragment_generator, Name}, _From, #state{fragments_generators = Gens} = State) ->
NGens = lists:keydelete(Name, 1, Gens),
{reply, ok, State#state{fragments_generators = NGens}};
handle_call(_Request, _From, State) ->
{reply, ok, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @end
%%--------------------------------------------------------------------
-spec handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}.
handle_cast(invalidate, State) ->
{noreply, State#state{definitions = none}};
handle_cast(_Request, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
-spec handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}.
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
-spec terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term().
terminate(_Reason, _State) ->
ok.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
-spec code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec get_definitions(#state{}) -> {#state{}, any()}.
get_definitions(#state{definitions = Defs, fragments_generators = Gens} = State) ->
DefaultOptions = [{<<"console commands">>,
{[ejabberd_ctl],
[{acl, all}],
{all, none}}},
{<<"admin access">>,
{[],
[{acl, admin}],
{all, [start, stop]}}}],
NDefs = case Defs of
none ->
ApiPerms = ejabberd_config:get_option(api_permissions, fun(A) -> A end, DefaultOptions),
AllCommands = ejabberd_commands:get_commands_definition(),
Frags = lists:foldl(
fun({_Name, Generator}, Acc) ->
Acc ++ Generator()
end, [], Gens),
lists:map(
fun({Name, {From, Who, {Add, Del}}}) ->
Cmds = filter_commands_with_permissions(AllCommands, Add, Del),
{Name, {From, Who, Cmds}}
end, ApiPerms ++ Frags);
V ->
V
end,
{State#state{definitions = NDefs}, NDefs}.
matches_definition({_Name, {From, Who, What}}, Cmd, Module, Host, CallerInfo) ->
case What == all orelse lists:member(Cmd, What) of
true ->
case From == [] orelse lists:member(Module, From) of
true ->
Scope = maps:get(oauth_scope, CallerInfo, none),
lists:any(
fun({access, Access}) when Scope == none ->
acl:access_matches(Access, CallerInfo, Host) == allow;
({acl, _} = Acl) when Scope == none ->
acl:acl_rule_matches(Acl, CallerInfo, Host);
({oauth, Scopes, List}) when Scope /= none ->
case ejabberd_oauth:scope_in_scope_list(Scope, Scopes) of
true ->
lists:any(
fun({access, Access}) ->
acl:access_matches(Access, CallerInfo, Host) == allow;
({acl, _} = Acl) ->
acl:acl_rule_matches(Acl, CallerInfo, Host)
end, List);
_ ->
false
end;
(_) ->
false
end, Who);
_ ->
false
end;
_ ->
false
end.
filter_commands_with_permissions(AllCommands, Add, Del) ->
CommandsAdd = filter_commands_with_patterns(AllCommands, Add, []),
CommandsDel = filter_commands_with_patterns(CommandsAdd, Del, []),
lists:map(fun(#ejabberd_commands{name = N}) -> N end,
CommandsAdd -- CommandsDel).
filter_commands_with_patterns([], _Patterns, Acc) ->
Acc;
filter_commands_with_patterns([C | CRest], Patterns, Acc) ->
case command_matches_patterns(C, Patterns) of
true ->
filter_commands_with_patterns(CRest, Patterns, [C | Acc]);
_ ->
filter_commands_with_patterns(CRest, Patterns, Acc)
end.
command_matches_patterns(_, all) ->
true;
command_matches_patterns(_, none) ->
false;
command_matches_patterns(_, []) ->
false;
command_matches_patterns(#ejabberd_commands{tags = Tags} = C, [{tag, Tag} | Tail]) ->
case lists:member(Tag, Tags) of
true ->
true;
_ ->
command_matches_patterns(C, Tail)
end;
command_matches_patterns(#ejabberd_commands{name = Name}, [Name | _Tail]) ->
true;
command_matches_patterns(C, [_ | Tail]) ->
command_matches_patterns(C, Tail).
%%%===================================================================
%%% Options parsing code
%%%===================================================================
parse_api_permissions(Data) when is_list(Data) ->
throw({replace_with, [parse_api_permission(Name, Args) || {Name, Args} <- Data]}).
parse_api_permission(Name, Args) ->
{From, Who, What} = case key_split(Args, [{from, []}, {who, none}, {what, []}]) of
{error, Msg} ->
report_error(<<"~s inside api_permission '~s' section">>, [Msg, Name]);
Val -> Val
end,
{Name, {parse_from(Name, From), parse_who(Name, Who, oauth), parse_what(Name, What)}}.
parse_from(_Name, Module) when is_atom(Module) ->
[Module];
parse_from(Name, Modules) when is_list(Modules) ->
lists:foreach(fun(Module) when is_atom(Module) ->
ok;
(Val) ->
report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>,
[Val, Name])
end, Modules),
Modules;
parse_from(Name, Val) ->
report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>,
[Val, Name]).
parse_who(Name, Atom, ParseOauth) when is_atom(Atom) ->
parse_who(Name, [Atom], ParseOauth);
parse_who(Name, Defs, ParseOauth) when is_list(Defs) ->
lists:map(
fun([{access, Val}]) ->
try acl:access_rules_validator(Val) of
Rule ->
{access, Rule}
catch
throw:{invalid_syntax, Msg} ->
report_error(<<"Invalid access rule: '~s' used inside 'who' section for api_permission '~s'">>,
[Msg, Name]);
throw:{replace_with, NVal} ->
{access, NVal};
error:_ ->
report_error(<<"Invalid access rule '~p' used inside 'who' section for api_permission '~s'">>,
[Val, Name])
end;
([{oauth, OauthList}]) when is_list(OauthList) ->
case ParseOauth of
oauth ->
Nested = parse_who(Name, lists:flatten(OauthList), scope),
{Scopes, Rest} = lists:partition(
fun({scope, _}) -> true;
(_) -> false
end, Nested),
case Scopes of
[] ->
report_error(<<"Oauth rule must contain at least one scope rule in 'who' section for api_permission '~s'">>,
[Name]);
_ ->
{oauth, lists:foldl(fun({scope, S}, A) -> S ++ A end, [], Scopes), Rest}
end;
scope ->
report_error(<<"Oauth rule can't be embeded inside other oauth rule in 'who' section for api_permission '~s'">>,
[Name])
end;
({scope, ScopeList}) ->
case ParseOauth of
oauth ->
report_error(<<"Scope can be included only inside oauth rule in 'who' section for api_permission '~s'">>,
[Name]);
scope ->
ScopeList2 = case ScopeList of
V when is_binary(V) -> [V];
V2 when is_list(V2) -> V2;
V3 ->
report_error(<<"Invalid value for scope '~p' in 'who' section for api_permission '~s'">>,
[V3, Name])
end,
{scope, ScopeList2}
end;
(Atom) when is_atom(Atom) ->
{acl, Atom};
([Other]) ->
try acl:normalize_spec(Other) of
Rule2 ->
{acl, Rule2}
catch
_:_ ->
report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
[Other, Name])
end;
(Invalid) ->
report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
[Invalid, Name])
end, Defs);
parse_who(Name, Val, _ParseOauth) ->
report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
[Val, Name]).
parse_what(Name, Binary) when is_binary(Binary) ->
parse_what(Name, [Binary]);
parse_what(Name, Defs) when is_list(Defs) ->
{A, D} = lists:foldl(
fun(Def, {Add, Del}) ->
case parse_single_what(Def) of
{error, Err} ->
report_error(<<"~s used in value '~p' in 'what' section for api_permission '~s'">>,
[Err, Def, Name]);
all ->
{case Add of none -> none; _ -> all end, Del};
{neg, all} ->
{none, all};
{neg, Value} ->
{Add, case Del of L when is_list(L) -> [Value | L]; L2 -> L2 end};
Value ->
{case Add of L when is_list(L) -> [Value | L]; L2 -> L2 end, Del}
end
end, {[], []}, Defs),
case {A, D} of
{[], _} ->
{none, all};
{A2, []} ->
{A2, none};
V ->
V
end;
parse_what(Name, Val) ->
report_error(<<"Invalid value '~p' used inside 'what' section for api_permission '~s'">>,
[Val, Name]).
parse_single_what(<<"*">>) ->
all;
parse_single_what(<<"!*">>) ->
{neg, all};
parse_single_what(<<"!", Rest/binary>>) ->
case parse_single_what(Rest) of
{neg, _} ->
{error, <<"Double negation">>};
{error, _} = Err ->
Err;
V ->
{neg, V}
end;
parse_single_what(<<"[tag:", Rest/binary>>) ->
case binary:split(Rest, <<"]">>) of
[TagName, <<"">>] ->
case parse_single_what(TagName) of
{error, _} = Err ->
Err;
V when is_atom(V) ->
{tag, V};
_ ->
{error, <<"Invalid tag">>}
end;
_ ->
{error, <<"Invalid tag">>}
end;
parse_single_what(Binary) when is_binary(Binary) ->
case is_valid_command_name(Binary) of
true ->
binary_to_atom(Binary, latin1);
_ ->
{error, <<"Invalid value">>}
end;
parse_single_what(_) ->
{error, <<"Invalid value">>}.
is_valid_command_name(<<>>) ->
false;
is_valid_command_name(Val) ->
is_valid_command_name2(Val).
is_valid_command_name2(<<>>) ->
true;
is_valid_command_name2(<<K:8, Rest/binary>>) when K >= $a andalso K =< $z orelse K == $_ ->
is_valid_command_name2(Rest);
is_valid_command_name2(_) ->
false.
key_split(Args, Fields) ->
{_, Order1, Results1, Required1} = lists:foldl(
fun({Field, Default}, {Idx, Order, Results, Required}) ->
{Idx + 1, maps:put(Field, Idx, Order), [Default | Results], Required};
(Field, {Idx, Order, Results, Required}) ->
{Idx + 1, maps:put(Field, Idx, Order), [none | Results], maps:put(Field, 1, Required)}
end, {1, #{}, [], #{}}, Fields),
key_split(Args, list_to_tuple(Results1), Order1, Required1, #{}).
key_split([], _Results, _Order, Required, _Duplicates) when map_size(Required) > 0 ->
parse_error(<<"Missing fields '~s">>, [str:join(maps:keys(Required), <<", ">>)]);
key_split([], Results, _Order, _Required, _Duplicates) ->
Results;
key_split([{Arg, Value} | Rest], Results, Order, Required, Duplicates) ->
case maps:find(Arg, Order) of
{ok, Idx} ->
case maps:is_key(Arg, Duplicates) of
false ->
Results2 = setelement(Idx, Results, Value),
key_split(Rest, Results2, Order, maps:remove(Arg, Required), maps:put(Arg, 1, Duplicates));
true ->
parse_error(<<"Duplicate field '~s'">>, [Arg])
end;
_ ->
parse_error(<<"Unknown field '~s'">>, [Arg])
end.
report_error(Format, Args) ->
throw({invalid_syntax, iolist_to_binary(io_lib:format(Format, Args))}).
parse_error(Format, Args) ->
{error, iolist_to_binary(io_lib:format(Format, Args))}.
opt_type(api_permissions) ->
fun parse_api_permissions/1;
opt_type(_) ->
[api_permissions].

View File

@ -87,6 +87,7 @@ get_commands_spec() ->
args = [], result = {res, rescode}},
#ejabberd_commands{name = reopen_log, tags = [logs, server],
desc = "Reopen the log files",
policy = admin,
module = ?MODULE, function = reopen_log,
args = [], result = {res, rescode}},
#ejabberd_commands{name = rotate_log, tags = [logs, server],
@ -129,6 +130,7 @@ get_commands_spec() ->
#ejabberd_commands{name = register, tags = [accounts],
desc = "Register a user",
policy = admin,
module = ?MODULE, function = register,
args = [{user, binary}, {host, binary}, {password, binary}],
result = {res, restuple}},
@ -378,13 +380,12 @@ register(User, Host, Password) ->
{atomic, ok} ->
{ok, io_lib:format("User ~s@~s successfully registered", [User, Host])};
{atomic, exists} ->
String = io_lib:format("User ~s@~s already registered at node ~p",
[User, Host, node()]),
{exists, String};
Msg = io_lib:format("User ~s@~s already registered", [User, Host]),
{error, conflict, 10090, Msg};
{error, Reason} ->
String = io_lib:format("Can't register user ~s@~s at node ~p: ~p",
[User, Host, node(), Reason]),
{cannot_register, String}
{error, cannot_register, 10001, String}
end.
unregister(User, Host) ->
@ -402,7 +403,8 @@ registered_vhosts() ->
reload_config() ->
ejabberd_config:reload_file(),
acl:start(),
shaper:start().
shaper:start(),
ejabberd_access_permissions:invalidate().
%%%
%%% Cluster management

View File

@ -45,16 +45,19 @@ start(normal, _Args) ->
write_pid_file(),
jid:start(),
start_apps(),
start_elixir_application(),
ejabberd:check_app(ejabberd),
randoms:start(),
db_init(),
start(),
translate:start(),
ejabberd_access_permissions:start_link(),
ejabberd_ctl:init(),
ejabberd_commands:init(),
ejabberd_admin:start(),
gen_mod:start(),
ext_mod:start(),
setup_if_elixir_conf_used(),
ejabberd_config:start(),
set_settings_from_config(),
acl:start(),
@ -74,6 +77,7 @@ start(normal, _Args) ->
ejabberd_oauth:start(),
gen_mod:start_modules(),
ejabberd_listener:start_listeners(),
register_elixir_config_hooks(),
?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]),
Sup;
start(_, _) ->
@ -221,6 +225,7 @@ start_apps() ->
ejabberd:start_app(fast_tls),
ejabberd:start_app(fast_xml),
ejabberd:start_app(stringprep),
http_p1:start(),
ejabberd:start_app(cache_tab).
opt_type(net_ticktime) ->
@ -237,3 +242,26 @@ opt_type(modules) ->
Mods)
end;
opt_type(_) -> [cluster_nodes, loglevel, modules, net_ticktime].
setup_if_elixir_conf_used() ->
case ejabberd_config:is_using_elixir_config() of
true -> 'Elixir.Ejabberd.Config.Store':start_link();
false -> ok
end.
register_elixir_config_hooks() ->
case ejabberd_config:is_using_elixir_config() of
true -> 'Elixir.Ejabberd.Config':start_hooks();
false -> ok
end.
start_elixir_application() ->
case ejabberd_config:is_elixir_enabled() of
true ->
case application:ensure_started(elixir) of
ok -> ok;
{error, _Msg} -> ?ERROR_MSG("Elixir application not started.", [])
end;
_ ->
ok
end.

View File

@ -450,7 +450,7 @@ password_to_scram(Password) ->
?SCRAM_DEFAULT_ITERATION_COUNT).
password_to_scram(Password, IterationCount) ->
Salt = crypto:rand_bytes(?SALT_LENGTH),
Salt = randoms:bytes(?SALT_LENGTH),
SaltedPassword = scram:salted_password(Password, Salt,
IterationCount),
StoredKey =

View File

@ -270,7 +270,7 @@ password_to_scram(Password) ->
?SCRAM_DEFAULT_ITERATION_COUNT).
password_to_scram(Password, IterationCount) ->
Salt = crypto:rand_bytes(?SALT_LENGTH),
Salt = randoms:bytes(?SALT_LENGTH),
SaltedPassword = scram:salted_password(Password, Salt,
IterationCount),
StoredKey =

View File

@ -406,7 +406,7 @@ password_to_scram(Password) ->
?SCRAM_DEFAULT_ITERATION_COUNT).
password_to_scram(Password, IterationCount) ->
Salt = crypto:rand_bytes(?SALT_LENGTH),
Salt = randoms:bytes(?SALT_LENGTH),
SaltedPassword = scram:salted_password(Password, Salt,
IterationCount),
StoredKey =

View File

@ -32,6 +32,7 @@
-protocol({xep, 78, '2.5'}).
-protocol({xep, 138, '2.0'}).
-protocol({xep, 198, '1.3'}).
-protocol({xep, 356, '7.1'}).
-update_info({update, 0}).
@ -48,10 +49,16 @@
send_element/2,
socket_type/0,
get_presence/1,
get_last_presence/1,
get_aux_field/2,
set_aux_field/3,
del_aux_field/2,
get_subscription/2,
get_queued_stanzas/1,
get_csi_state/1,
set_csi_state/2,
get_resume_timeout/1,
set_resume_timeout/2,
send_filtered/5,
broadcast/4,
get_subscribed/1,
@ -112,9 +119,12 @@
mgmt_pending_since,
mgmt_timeout,
mgmt_max_timeout,
mgmt_ack_timeout,
mgmt_ack_timer,
mgmt_resend,
mgmt_stanzas_in = 0,
mgmt_stanzas_out = 0,
mgmt_stanzas_req = 0,
ask_offline = true,
lang = <<"">>}).
@ -182,6 +192,9 @@ socket_type() -> xml_stream.
get_presence(FsmRef) ->
(?GEN_FSM):sync_send_all_state_event(FsmRef,
{get_presence}, 1000).
get_last_presence(FsmRef) ->
(?GEN_FSM):sync_send_all_state_event(FsmRef,
{get_last_presence}, 1000).
-spec get_aux_field(any(), state()) -> {ok, any()} | error.
get_aux_field(Key, #state{aux_fields = Opts}) ->
@ -218,6 +231,27 @@ get_subscription(LFrom, StateData) ->
true -> none
end.
get_queued_stanzas(#state{mgmt_queue = Queue} = StateData) ->
lists:map(fun({_N, Time, El}) ->
add_resent_delay_info(StateData, El, Time)
end, queue:to_list(Queue)).
get_csi_state(#state{csi_state = CsiState}) ->
CsiState.
set_csi_state(#state{} = StateData, CsiState) ->
StateData#state{csi_state = CsiState};
set_csi_state(FsmRef, CsiState) ->
FsmRef ! {set_csi_state, CsiState}.
get_resume_timeout(#state{mgmt_timeout = Timeout}) ->
Timeout.
set_resume_timeout(#state{} = StateData, Timeout) ->
StateData#state{mgmt_timeout = Timeout};
set_resume_timeout(FsmRef, Timeout) ->
FsmRef ! {set_resume_timeout, Timeout}.
-spec send_filtered(pid(), binary(), jid(), jid(), stanza()) -> any().
send_filtered(FsmRef, Feature, From, To, Packet) ->
FsmRef ! {send_filtered, Feature, From, To, Packet}.
@ -282,13 +316,18 @@ init([{SockMod, Socket}, Opts]) ->
_ -> 1000
end,
ResumeTimeout = case proplists:get_value(resume_timeout, Opts) of
Timeout when is_integer(Timeout), Timeout >= 0 -> Timeout;
RTimeo when is_integer(RTimeo), RTimeo >= 0 -> RTimeo;
_ -> 300
end,
MaxResumeTimeout = case proplists:get_value(max_resume_timeout, Opts) of
Max when is_integer(Max), Max >= ResumeTimeout -> Max;
_ -> ResumeTimeout
end,
AckTimeout = case proplists:get_value(ack_timeout, Opts) of
ATimeo when is_integer(ATimeo), ATimeo > 0 -> ATimeo * 1000;
infinity -> undefined;
_ -> 60000
end,
ResendOnTimeout = case proplists:get_value(resend_on_timeout, Opts) of
Resend when is_boolean(Resend) -> Resend;
if_offline -> if_offline;
@ -312,6 +351,7 @@ init([{SockMod, Socket}, Opts]) ->
mgmt_max_queue = MaxAckQueue,
mgmt_timeout = ResumeTimeout,
mgmt_max_timeout = MaxResumeTimeout,
mgmt_ack_timeout = AckTimeout,
mgmt_resend = ResendOnTimeout},
{ok, wait_for_stream, StateData, ?C2S_OPEN_TIMEOUT}.
@ -1147,6 +1187,15 @@ handle_sync_event({get_presence}, _From, StateName,
Resource = StateData#state.resource,
Reply = {User, Resource, Show, Status},
fsm_reply(Reply, StateName, StateData);
handle_sync_event({get_last_presence}, _From, StateName,
StateData) ->
User = StateData#state.user,
Server = StateData#state.server,
PresLast = StateData#state.pres_last,
Resource = StateData#state.resource,
Reply = {User, Server, Resource, PresLast},
fsm_reply(Reply, StateName, StateData);
handle_sync_event(get_subscribed, _From, StateName,
StateData) ->
Subscribed = (?SETS):to_list(StateData#state.pres_f),
@ -1159,7 +1208,7 @@ handle_sync_event({resume_session, Time}, _From, _StateName,
StateData#state.user,
StateData#state.server,
StateData#state.resource),
{stop, normal, {ok, StateData}, StateData#state{mgmt_state = resumed}};
{stop, normal, {resume, StateData}, StateData#state{mgmt_state = resumed}};
handle_sync_event({resume_session, _Time}, _From, StateName,
StateData) ->
{reply, {error, <<"Previous session not found">>}, StateName, StateData};
@ -1347,8 +1396,13 @@ handle_info({route, From, To, Packet}, StateName, StateData) when ?is_stanza(Pac
groupchat -> ok;
headline -> ok;
_ ->
case xmpp:has_subtag(Packet, #muc_user{}) of
true ->
ok;
false ->
ejabberd_router:route_error(
To, From, Packet, xmpp:err_service_unavailable())
end
end,
{false, StateData}
end
@ -1444,8 +1498,24 @@ handle_info({broadcast, Type, From, Packet}, StateName, StateData) ->
From, jid:make(USR), Packet)
end, lists:usort(Recipients)),
fsm_next_state(StateName, StateData);
handle_info({set_csi_state, CsiState}, StateName, StateData) ->
fsm_next_state(StateName, StateData#state{csi_state = CsiState});
handle_info({set_resume_timeout, Timeout}, StateName, StateData) ->
fsm_next_state(StateName, StateData#state{mgmt_timeout = Timeout});
handle_info(dont_ask_offline, StateName, StateData) ->
fsm_next_state(StateName, StateData#state{ask_offline = false});
handle_info(close, StateName, StateData) ->
?DEBUG("Timeout waiting for stream management acknowledgement of ~s",
[jid:to_string(StateData#state.jid)]),
close(self()),
fsm_next_state(StateName, StateData#state{mgmt_ack_timer = undefined});
handle_info({_Ref, {resume, OldStateData}}, StateName, StateData) ->
%% This happens if the resume_session/1 request timed out; the new session
%% now receives the late response.
?DEBUG("Received old session state for ~s after failed resumption",
[jid:to_string(OldStateData#state.jid)]),
handle_unacked_stanzas(OldStateData#state{mgmt_resend = false}),
fsm_next_state(StateName, StateData);
handle_info(Info, StateName, StateData) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
fsm_next_state(StateName, StateData).
@ -1562,6 +1632,7 @@ send_text(StateData, Text) ->
send_element(StateData, El) when StateData#state.mgmt_state == pending ->
?DEBUG("Cannot send element while waiting for resumption: ~p", [El]);
send_element(StateData, #xmlel{} = El) when StateData#state.xml_socket ->
?DEBUG("Send XML on stream = ~p", [fxml:element_to_binary(El)]),
(StateData#state.sockmod):send_xml(StateData#state.socket,
{xmlstreamelement, El});
send_element(StateData, #xmlel{} = El) ->
@ -1585,8 +1656,8 @@ send_stanza(StateData, Stanza) when StateData#state.csi_state == inactive ->
send_stanza(StateData, Stanza) when StateData#state.mgmt_state == pending ->
mgmt_queue_add(StateData, Stanza);
send_stanza(StateData, Stanza) when StateData#state.mgmt_state == active ->
NewStateData = send_stanza_and_ack_req(StateData, Stanza),
mgmt_queue_add(NewStateData, Stanza);
NewStateData = mgmt_queue_add(StateData, Stanza),
mgmt_send_stanza(NewStateData, Stanza);
send_stanza(StateData, Stanza) ->
send_element(StateData, Stanza),
StateData.
@ -2101,13 +2172,25 @@ fsm_next_state(session_established, StateData) ->
?C2S_HIBERNATE_TIMEOUT};
fsm_next_state(wait_for_resume, #state{mgmt_timeout = 0} = StateData) ->
{stop, normal, StateData};
fsm_next_state(wait_for_resume, #state{mgmt_pending_since = undefined} =
StateData) ->
fsm_next_state(wait_for_resume, #state{mgmt_pending_since = undefined,
sid = SID, jid = JID, ip = IP,
conn = Conn, auth_module = AuthModule,
server = Host} = StateData) ->
case StateData of
#state{mgmt_ack_timer = undefined} ->
ok;
#state{mgmt_ack_timer = Timer} ->
erlang:cancel_timer(Timer)
end,
?INFO_MSG("Waiting for resumption of stream for ~s",
[jid:to_string(StateData#state.jid)]),
[jid:to_string(JID)]),
Info = [{ip, IP}, {conn, Conn}, {auth_module, AuthModule}],
NewStateData = ejabberd_hooks:run_fold(c2s_session_pending, Host, StateData,
[SID, JID, Info]),
{next_state, wait_for_resume,
StateData#state{mgmt_state = pending, mgmt_pending_since = os:timestamp()},
StateData#state.mgmt_timeout};
NewStateData#state{mgmt_state = pending,
mgmt_pending_since = os:timestamp()},
NewStateData#state.mgmt_timeout};
fsm_next_state(wait_for_resume, StateData) ->
Diff = timer:now_diff(os:timestamp(), StateData#state.mgmt_pending_since),
Timeout = max(StateData#state.mgmt_timeout - Diff div 1000, 1),
@ -2338,15 +2421,16 @@ handle_r(StateData) ->
-spec handle_a(state(), sm_a()) -> state().
handle_a(StateData, #sm_a{h = H}) ->
check_h_attribute(StateData, H).
NewStateData = check_h_attribute(StateData, H),
maybe_renew_ack_request(NewStateData).
-spec handle_resume(state(), sm_resume()) -> {ok, state()} | error.
handle_resume(StateData, #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) ->
R = case stream_mgmt_enabled(StateData) of
true ->
case inherit_session_state(StateData, PrevID) of
{ok, InheritedState} ->
{ok, InheritedState, H};
{ok, InheritedState, Info} ->
{ok, InheritedState, Info, H};
{error, Err, InH} ->
{error, #sm_failed{reason = 'item-not-found',
h = InH, xmlns = Xmlns}, Err};
@ -2360,7 +2444,7 @@ handle_resume(StateData, #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) ->
<<"XEP-0198 disabled">>}
end,
case R of
{ok, ResumedState, NumHandled} ->
{ok, ResumedState, ResumedInfo, NumHandled} ->
NewState = check_h_attribute(ResumedState, NumHandled),
AttrXmlns = NewState#state.mgmt_xmlns,
AttrId = make_resume_id(NewState),
@ -2374,11 +2458,16 @@ handle_resume(StateData, #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) ->
end,
handle_unacked_stanzas(NewState, SendFun),
send_element(NewState, #sm_r{xmlns = AttrXmlns}),
FlushedState = csi_flush_queue(NewState),
NewStateData = FlushedState#state{csi_state = active},
NewState1 = csi_flush_queue(NewState),
NewState2 = ejabberd_hooks:run_fold(c2s_session_resumed,
StateData#state.server,
NewState1,
[NewState1#state.sid,
NewState1#state.jid,
ResumedInfo]),
?INFO_MSG("Resumed session for ~s",
[jid:to_string(NewStateData#state.jid)]),
{ok, NewStateData};
[jid:to_string(NewState2#state.jid)]),
{ok, NewState2};
{error, El, Msg} ->
send_element(StateData, El),
?INFO_MSG("Cannot resume session for ~s@~s: ~s",
@ -2413,17 +2502,47 @@ update_num_stanzas_in(#state{mgmt_state = MgmtState} = StateData, El)
update_num_stanzas_in(StateData, _El) ->
StateData.
-spec send_stanza_and_ack_req(state(), stanza()) -> state().
send_stanza_and_ack_req(StateData, Stanza) ->
AckReq = #sm_r{xmlns = StateData#state.mgmt_xmlns},
case send_element(StateData, Stanza) == ok andalso
send_element(StateData, AckReq) == ok of
true ->
StateData;
false ->
mgmt_send_stanza(StateData, Stanza) ->
case send_element(StateData, Stanza) of
ok ->
maybe_request_ack(StateData);
_ ->
StateData#state{mgmt_state = pending}
end.
maybe_request_ack(#state{mgmt_ack_timer = undefined} = StateData) ->
request_ack(StateData);
maybe_request_ack(StateData) ->
StateData.
request_ack(#state{mgmt_xmlns = Xmlns,
mgmt_ack_timeout = AckTimeout} = StateData) ->
AckReq = #sm_r{xmlns = Xmlns},
case {send_element(StateData, AckReq), AckTimeout} of
{ok, undefined} ->
ok;
{ok, Timeout} ->
Timer = erlang:send_after(Timeout, self(), close),
StateData#state{mgmt_ack_timer = Timer,
mgmt_stanzas_req = StateData#state.mgmt_stanzas_out};
_ ->
StateData#state{mgmt_state = pending}
end.
maybe_renew_ack_request(#state{mgmt_ack_timer = undefined} = StateData) ->
StateData;
maybe_renew_ack_request(#state{mgmt_ack_timer = Timer,
mgmt_queue = Queue,
mgmt_stanzas_out = NumStanzasOut,
mgmt_stanzas_req = NumStanzasReq} = StateData) ->
erlang:cancel_timer(Timer),
case NumStanzasReq < NumStanzasOut andalso not queue:is_empty(Queue) of
true ->
request_ack(StateData#state{mgmt_ack_timer = undefined});
false ->
StateData#state{mgmt_ack_timer = undefined}
end.
-spec mgmt_queue_add(state(), xmpp_element()) -> state().
mgmt_queue_add(StateData, El) ->
NewNum = case StateData#state.mgmt_stanzas_out of
@ -2473,7 +2592,12 @@ handle_unacked_stanzas(#state{mgmt_state = MgmtState} = StateData, F)
fun({_, Time, Pkt}) ->
From = xmpp:get_from(Pkt),
To = xmpp:get_to(Pkt),
F(From, To, Pkt, Time)
case {From, To} of
{#jid{}, #jid{}} ->
F(From, To, Pkt, Time);
{_, _} ->
?DEBUG("Dropping stanza due to invalid JID(s)", [])
end
end, queue:to_list(Queue))
end;
handle_unacked_stanzas(_StateData, _F) ->
@ -2540,7 +2664,8 @@ handle_unacked_stanzas(#state{mgmt_state = MgmtState} = StateData)
[StateData, From,
StateData#state.jid, El]) of
true ->
ok;
?DEBUG("Dropping archived message stanza from ~p",
[jid:to_string(xmpp:get_from(El))]);
false ->
ReRoute(From, To, El, Time)
end
@ -2580,7 +2705,7 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) ->
OldPID ->
OldSID = {Time, OldPID},
case catch resume_session(OldSID) of
{ok, OldStateData} ->
{resume, OldStateData} ->
NewSID = {Time, self()}, % Old time, new PID
Priority = case OldStateData#state.pres_last of
undefined ->
@ -2604,13 +2729,13 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) ->
pres_timestamp = OldStateData#state.pres_timestamp,
privacy_list = OldStateData#state.privacy_list,
aux_fields = OldStateData#state.aux_fields,
csi_state = OldStateData#state.csi_state,
mgmt_xmlns = OldStateData#state.mgmt_xmlns,
mgmt_queue = OldStateData#state.mgmt_queue,
mgmt_timeout = OldStateData#state.mgmt_timeout,
mgmt_stanzas_in = OldStateData#state.mgmt_stanzas_in,
mgmt_stanzas_out = OldStateData#state.mgmt_stanzas_out,
mgmt_state = active}};
mgmt_state = active,
csi_state = active}, Info};
{error, Msg} ->
{error, Msg};
_ ->
@ -2623,7 +2748,7 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) ->
-spec resume_session({integer(), pid()}) -> any().
resume_session({Time, PID}) ->
(?GEN_FSM):sync_send_all_state_event(PID, {resume_session, Time}, 5000).
(?GEN_FSM):sync_send_all_state_event(PID, {resume_session, Time}, 15000).
-spec make_resume_id(state()) -> binary().
make_resume_id(StateData) ->
@ -2640,11 +2765,11 @@ add_resent_delay_info(#state{server = From}, El, Time) ->
%%% XEP-0352
%%%----------------------------------------------------------------------
-spec csi_filter_stanza(state(), stanza()) -> state().
csi_filter_stanza(#state{csi_state = CsiState, server = Server} = StateData,
Stanza) ->
csi_filter_stanza(#state{csi_state = CsiState, jid = JID, server = Server} =
StateData, Stanza) ->
{StateData1, Stanzas} = ejabberd_hooks:run_fold(csi_filter_stanza, Server,
{StateData, [Stanza]},
[Server, Stanza]),
[Server, JID, Stanza]),
StateData2 = lists:foldl(fun(CurStanza, AccState) ->
send_stanza(AccState, CurStanza)
end, StateData1#state{csi_state = active},
@ -2652,9 +2777,11 @@ csi_filter_stanza(#state{csi_state = CsiState, server = Server} = StateData,
StateData2#state{csi_state = CsiState}.
-spec csi_flush_queue(state()) -> state().
csi_flush_queue(#state{csi_state = CsiState, server = Server} = StateData) ->
csi_flush_queue(#state{csi_state = CsiState, jid = JID, server = Server} =
StateData) ->
{StateData1, Stanzas} = ejabberd_hooks:run_fold(csi_flush_queue, Server,
{StateData, []}, [Server]),
{StateData, []},
[Server, JID]),
StateData2 = lists:foldl(fun(CurStanza, AccState) ->
send_stanza(AccState, CurStanza)
end, StateData1#state{csi_state = active},

View File

@ -218,22 +218,26 @@
get_command_format/1,
get_command_format/2,
get_command_format/3,
get_command_policy/1,
get_command_policy_and_scope/1,
get_command_definition/1,
get_command_definition/2,
get_tags_commands/0,
get_tags_commands/1,
get_commands/0,
get_exposed_commands/0,
register_commands/1,
unregister_commands/1,
expose_commands/1,
execute_command/2,
execute_command/3,
execute_command/4,
execute_command/5,
execute_command/6,
opt_type/1,
get_commands_spec/0
]).
get_commands_spec/0,
get_commands_definition/0,
get_commands_definition/1,
execute_command2/3,
execute_command2/4]).
-include("ejabberd_commands.hrl").
-include("ejabberd.hrl").
@ -273,28 +277,32 @@ get_commands_spec() ->
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
result_example = ok}].
init() ->
mnesia:delete_table(ejabberd_commands),
mnesia:create_table(ejabberd_commands,
[{ram_copies, [node()]},
{local_content, true},
{attributes, record_info(fields, ejabberd_commands)},
{type, bag}]),
mnesia:add_table_copy(ejabberd_commands, node(), ram_copies),
register_commands(get_commands_spec()).
register_commands(get_commands_spec()),
ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0).
-spec register_commands([ejabberd_commands()]) -> ok.
%% @doc Register ejabberd commands.
%% If a command is already registered, a warning is printed and the
%% old command is preserved.
%% A registered command is not directly available to be called through
%% ejabberd ReST API. It need to be exposed to be available through API.
register_commands(Commands) ->
lists:foreach(
fun(Command) ->
% XXX check if command exists
%% XXX check if command exists
mnesia:dirty_write(Command)
% ?DEBUG("This command is already defined:~n~p", [Command])
%% ?DEBUG("This command is already defined:~n~p", [Command])
end,
Commands).
Commands),
ejabberd_access_permissions:invalidate(),
ok.
-spec unregister_commands([ejabberd_commands()]) -> ok.
@ -304,7 +312,28 @@ unregister_commands(Commands) ->
fun(Command) ->
mnesia:dirty_delete_object(Command)
end,
Commands).
Commands),
ejabberd_access_permissions:invalidate(),
ok.
%% @doc Expose command through ejabberd ReST API.
%% Pass a list of command names or policy to expose.
-spec expose_commands([ejabberd_commands()|atom()|open|user|admin|restricted]) -> ok | {error, atom()}.
expose_commands(Commands) ->
Names = lists:map(fun(#ejabberd_commands{name = Name}) ->
Name;
(Name) when is_atom(Name) ->
Name
end,
Commands),
case ejabberd_config:add_local_option(commands, [{add_commands, Names}]) of
{aborted, Reason} ->
{error, Reason};
{atomic, Result} ->
Result
end.
-spec list_commands() -> [{atom(), [aterm()], string()}].
@ -366,17 +395,23 @@ get_command_format(Name, Auth, Version) ->
{Args, Result}
end.
-spec get_command_policy(atom()) -> {ok, open|user|admin|restricted} | {error, command_not_found}.
-spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}.
%% @doc return command policy.
get_command_policy(Name) ->
get_command_policy_and_scope(Name) ->
case get_command_definition(Name) of
#ejabberd_commands{policy = Policy} ->
{ok, Policy};
#ejabberd_commands{policy = Policy} = Cmd ->
{ok, Policy, cmd_scope(Cmd)};
command_not_found ->
{error, command_not_found}
end.
%% The oauth scopes for a command are the command name itself,
%% also might include either 'ejabberd:user' or 'ejabberd:admin'
cmd_scope(#ejabberd_commands{policy = Policy, name = Name}) ->
[erlang:atom_to_binary(Name,utf8)] ++ [<<"ejabberd:user">> || Policy == user] ++ [<<"ejabberd:admin">> || Policy == admin].
-spec get_command_definition(atom()) -> ejabberd_commands().
%% @doc Get the definition record of a command.
@ -397,9 +432,12 @@ get_command_definition(Name, Version) ->
{V, C}
end)))) of
[{_, Command} | _ ] -> Command;
_E -> throw(unknown_command)
_E -> throw({error, unknown_command})
end.
get_commands_definition() ->
get_commands_definition(?DEFAULT_VERSION).
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
% @doc Returns all commands for a given API version
@ -421,13 +459,25 @@ get_commands_definition(Version) ->
end,
lists:foldl(F, [], L).
execute_command2(Name, Arguments, CallerInfo) ->
execute_command2(Name, Arguments, CallerInfo, ?DEFAULT_VERSION).
execute_command2(Name, Arguments, CallerInfo, Version) ->
Command = get_command_definition(Name, Version),
case ejabberd_access_permissions:can_access(Name, CallerInfo) of
allow ->
do_execute_command(Command, Arguments);
_ ->
throw({error, access_rules_unauthorized})
end.
%% @spec (Name::atom(), Arguments) -> ResultTerm
%% where
%% Arguments = [any()]
%% @doc Execute a command.
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data |
%% no_auth_provided
%% no_auth_provided | access_rules_unauthorized
execute_command(Name, Arguments) ->
execute_command(Name, Arguments, ?DEFAULT_VERSION).
@ -488,7 +538,7 @@ execute_command(AccessCommands, Auth, Name, Arguments) ->
%%
%% @doc Execute a command in a given API version
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided | access_rules_unauthorized
execute_command(AccessCommands1, Auth1, Name, Arguments, Version) ->
execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}).
@ -497,32 +547,55 @@ execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) ->
true -> admin;
false -> Auth1
end,
TokenJID = oauth_token_user(Auth1),
Command = get_command_definition(Name, Version),
AccessCommands = get_access_commands(AccessCommands1, Version),
AccessCommands = get_all_access_commands(AccessCommands1),
case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of
ok -> execute_command2(Auth, Command, Arguments)
ok -> execute_check_policy(Auth, TokenJID, Command, Arguments)
end.
execute_command2(
_Auth, #ejabberd_commands{policy = open} = Command, Arguments) ->
execute_command2(Command, Arguments);
execute_command2(
_Auth, #ejabberd_commands{policy = restricted} = Command, Arguments) ->
execute_command2(Command, Arguments);
execute_command2(
_Auth, #ejabberd_commands{policy = admin} = Command, Arguments) ->
execute_command2(Command, Arguments);
execute_command2(
admin, #ejabberd_commands{policy = user} = Command, Arguments) ->
execute_command2(Command, Arguments);
execute_command2(
noauth, #ejabberd_commands{policy = user} = Command, Arguments) ->
execute_command2(Command, Arguments);
execute_command2(
{User, Server, _, _}, #ejabberd_commands{policy = user} = Command, Arguments) ->
execute_command2(Command, [User, Server | Arguments]).
execute_command2(Command, Arguments) ->
execute_check_policy(
_Auth, _JID, #ejabberd_commands{policy = open} = Command, Arguments) ->
do_execute_command(Command, Arguments);
execute_check_policy(
noauth, _JID, Command, Arguments) ->
do_execute_command(Command, Arguments);
execute_check_policy(
_Auth, _JID, #ejabberd_commands{policy = restricted} = Command, Arguments) ->
do_execute_command(Command, Arguments);
execute_check_policy(
_Auth, JID, #ejabberd_commands{policy = admin} = Command, Arguments) ->
execute_check_access(JID, Command, Arguments);
execute_check_policy(
admin, JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
execute_check_access(JID, Command, Arguments);
execute_check_policy(
{User, Server, _, _}, JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
execute_check_access(JID, Command, [User, Server | Arguments]).
execute_check_access(_FromJID, #ejabberd_commands{access = []} = Command, Arguments) ->
do_execute_command(Command, Arguments);
execute_check_access(undefined, _Command, _Arguments) ->
throw({error, access_rules_unauthorized});
execute_check_access(FromJID, #ejabberd_commands{access = AccessRefs} = Command, Arguments) ->
%% TODO Review: Do we have smarter / better way to check rule on other Host than global ?
Host = global,
Rules = lists:map(fun({Mod, AccessName, Default}) ->
gen_mod:get_module_opt(Host, Mod,
AccessName, fun(A) -> A end, Default);
(Default) ->
Default
end, AccessRefs),
case acl:any_rules_allowed(Host, Rules, FromJID) of
true ->
do_execute_command(Command, Arguments);
false ->
throw({error, access_rules_unauthorized})
end.
do_execute_command(Command, Arguments) ->
Module = Command#ejabberd_commands.module,
Function = Command#ejabberd_commands.function,
?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]),
@ -627,11 +700,11 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI
check_auth(_Command, noauth) ->
no_auth_provided;
check_auth(Command, {User, Server, {oauth, Token}, _}) ->
Scope = erlang:atom_to_binary(Command#ejabberd_commands.name, utf8),
case ejabberd_oauth:check_token(User, Server, Scope, Token) of
ScopeList = cmd_scope(Command),
case ejabberd_oauth:check_token(User, Server, ScopeList, Token) of
true ->
{ok, User, Server};
false ->
_ ->
throw({error, invalid_account_data})
end;
check_auth(_Command, {User, Server, Password, _}) when is_binary(Password) ->
@ -705,15 +778,19 @@ tag_arguments(ArgsDefs, Args) ->
Args).
%% Get commands for all version
get_all_access_commands(AccessCommands) ->
get_access_commands(AccessCommands, ?DEFAULT_VERSION).
get_access_commands(undefined, Version) ->
Cmds = get_commands(Version),
Cmds = get_exposed_commands(Version),
[{?POLICY_ACCESS, Cmds, []}];
get_access_commands(AccessCommands, _Version) ->
AccessCommands.
get_commands() ->
get_commands(?DEFAULT_VERSION).
get_commands(Version) ->
get_exposed_commands() ->
get_exposed_commands(?DEFAULT_VERSION).
get_exposed_commands(Version) ->
Opts0 = ejabberd_config:get_option(
commands,
fun(V) when is_list(V) -> V end,
@ -727,31 +804,38 @@ get_commands(Version) ->
Cmds =
lists:foldl(
fun([{add_commands, L}], Acc) ->
Cmds = case L of
open -> OpenCmds;
restricted -> RestrictedCmds;
admin -> AdminCmds;
user -> UserCmds;
_ when is_list(L) -> L
end,
Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
lists:usort(Cmds ++ Acc);
([{remove_commands, L}], Acc) ->
Cmds = case L of
open -> OpenCmds;
restricted -> RestrictedCmds;
admin -> AdminCmds;
user -> UserCmds;
_ when is_list(L) -> L
end,
Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
Acc -- Cmds;
(_, Acc) -> Acc
end, AdminCmds ++ UserCmds, Opts),
end, [], Opts),
Cmds.
%% This is used to allow mixing command policy (like open, user, admin, restricted), with command entry
expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) when is_list(L) ->
lists:foldl(fun(open, Acc) -> OpenCmds ++ Acc;
(user, Acc) -> UserCmds ++ Acc;
(admin, Acc) -> AdminCmds ++ Acc;
(restricted, Acc) -> RestrictedCmds ++ Acc;
(Command, Acc) when is_atom(Command) ->
[Command|Acc]
end, [], L).
oauth_token_user(noauth) ->
undefined;
oauth_token_user(admin) ->
undefined;
oauth_token_user({User, Server, _, _}) ->
jid:make(User, Server, <<>>).
is_admin(_Name, admin, _Extra) ->
true;
is_admin(_Name, {_User, _Server, _, false}, _Extra) ->
false;
is_admin(_Name, Map, _extra) when is_map(Map) ->
true;
is_admin(Name, Auth, Extra) ->
{ACLInfo, Server} = case Auth of
{U, S, _, _} ->
@ -773,6 +857,14 @@ is_admin(Name, Auth, Extra) ->
deny -> false
end.
permission_addon() ->
[{<<"'commands' option compatibility shim">>,
{[],
[{access, ejabberd_config:get_option(commands_admin_access,
fun(V) -> V end,
none)}],
{get_exposed_commands(), []}}}].
opt_type(commands_admin_access) -> fun acl:access_rules_validator/1;
opt_type(commands) ->
fun(V) when is_list(V) -> V end;

View File

@ -33,10 +33,12 @@
get_option/2, get_option/3, add_option/2, has_option/1,
get_vh_by_auth_method/1, is_file_readable/1,
get_version/0, get_myhosts/0, get_mylang/0,
get_ejabberd_config_path/0, is_using_elixir_config/0,
prepare_opt_val/4, convert_table_to_binary/5,
transform_options/1, collect_options/1, default_db/2,
convert_to_yaml/1, convert_to_yaml/2, v_db/2,
env_binary_to_list/2, opt_type/1, may_hide_data/1]).
env_binary_to_list/2, opt_type/1, may_hide_data/1,
is_elixir_enabled/0, v_dbs/1, v_dbs_mods/1]).
-export([start/2]).
@ -147,7 +149,18 @@ read_file(File) ->
{include_modules_configs, true}]).
read_file(File, Opts) ->
Terms1 = get_plain_terms_file(File, Opts),
Terms1 = case is_elixir_enabled() of
true ->
case 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(File) of
true ->
'Elixir.Ejabberd.Config':init(File),
'Elixir.Ejabberd.Config':get_ejabberd_opts();
false ->
get_plain_terms_file(File, Opts)
end;
false ->
get_plain_terms_file(File, Opts)
end,
Terms_macros = case proplists:get_bool(replace_macros, Opts) of
true -> replace_macros(Terms1);
false -> Terms1
@ -165,7 +178,8 @@ read_file(File, Opts) ->
-spec load_file(string()) -> ok.
load_file(File) ->
State = read_file(File),
State0 = read_file(File),
State = validate_opts(State0),
set_opts(State).
-spec reload_file() -> ok.
@ -318,7 +332,9 @@ get_absolute_path(File) ->
File;
relative ->
{ok, Dir} = file:get_cwd(),
filename:absname_join(Dir, File)
filename:absname_join(Dir, File);
volumerelative ->
filename:absname(File)
end.
@ -877,7 +893,20 @@ v_db(Mod, Type) ->
[] -> erlang:error(badarg)
end.
-spec default_db(global | binary(), module()) -> atom().
-spec v_dbs(module()) -> [atom()].
v_dbs(Mod) ->
lists:flatten(ets:match(module_db, {Mod, '$1'})).
-spec v_dbs_mods(module()) -> [module()].
v_dbs_mods(Mod) ->
lists:map(fun([M]) ->
binary_to_atom(<<(atom_to_binary(Mod, utf8))/binary, "_",
(atom_to_binary(M, utf8))/binary>>, utf8)
end, ets:match(module_db, {Mod, '$1'})).
-spec default_db(binary(), module()) -> atom().
default_db(Host, Module) ->
case ejabberd_config:get_option(
@ -1010,7 +1039,6 @@ replace_module(mod_private_odbc) -> {mod_private, sql};
replace_module(mod_roster_odbc) -> {mod_roster, sql};
replace_module(mod_shared_roster_odbc) -> {mod_shared_roster, sql};
replace_module(mod_vcard_odbc) -> {mod_vcard, sql};
replace_module(mod_vcard_ldap) -> {mod_vcard, ldap};
replace_module(mod_vcard_xupdate_odbc) -> {mod_vcard_xupdate, sql};
replace_module(mod_pubsub_odbc) -> {mod_pubsub, sql};
replace_module(Module) ->
@ -1041,6 +1069,23 @@ replace_modules(Modules) ->
%% Elixir module naming
%% ====================
-ifdef(ELIXIR_ENABLED).
is_elixir_enabled() ->
true.
-else.
is_elixir_enabled() ->
false.
-endif.
is_using_elixir_config() ->
case is_elixir_enabled() of
true ->
Config = get_ejabberd_config_path(),
'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config);
false ->
false
end.
%% If module name start with uppercase letter, this is an Elixir module:
is_elixir_module(Module) ->
case atom_to_list(Module) of

View File

@ -321,9 +321,14 @@ call_command([CmdString | Args], Auth, AccessCommands, Version) ->
{ArgsFormat, ResultFormat} ->
case (catch format_args(Args, ArgsFormat)) of
ArgsFormatted when is_list(ArgsFormatted) ->
Result = ejabberd_commands:execute_command(AccessCommands,
Auth, Command,
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], _} | _]}} ->
@ -374,6 +379,12 @@ format_arg2(Arg, Parse)->
format_result({error, ErrorAtom}, _) ->
{io_lib:format("Error: ~p", [ErrorAtom]), make_status(error)};
%% An error should always be allowed to return extended error to help with API.
%% Extended error is of the form:
%% {error, type :: atom(), code :: int(), Desc :: string()}
format_result({error, ErrorAtom, Code, _Msg}, _) ->
{io_lib:format("Error: ~p", [ErrorAtom]), make_status(Code)};
format_result(Atom, {_Name, atom}) ->
io_lib:format("~p", [Atom]);
@ -433,6 +444,8 @@ format_result(404, {_Name, _}) ->
make_status(ok) -> ?STATUS_SUCCESS;
make_status(true) -> ?STATUS_SUCCESS;
make_status(Code) when is_integer(Code), Code > 255 -> ?STATUS_ERROR;
make_status(Code) when is_integer(Code), Code > 0 -> Code;
make_status(_Error) -> ?STATUS_ERROR.
get_list_commands(Version) ->

View File

@ -145,9 +145,14 @@ init({SockMod, Socket}, Opts) ->
DefinedHandlers = gen_mod:get_opt(
request_handlers, Opts,
fun(Hs) ->
Hs1 = lists:map(fun
({Mod, Path}) when is_atom(Mod) -> {Path, Mod};
({Path, Mod}) -> {Path, Mod}
end, Hs),
[{str:tokens(
iolist_to_binary(Path), <<"/">>),
Mod} || {Path, Mod} <- Hs]
Mod} || {Path, Mod} <- Hs1]
end, []),
RequestHandlers = DefinedHandlers ++ Captcha ++ Register ++
Admin ++ Bind ++ XMLRPC,
@ -391,7 +396,9 @@ extract_path_query(#state{request_method = Method,
socket = _Socket} = State)
when (Method =:= 'POST' orelse Method =:= 'PUT') andalso
is_integer(Len) ->
{NewState, Data} = recv_data(State, Len),
case recv_data(State, Len) of
error -> {State, false};
{NewState, Data} ->
?DEBUG("client data: ~p~n", [Data]),
case catch url_decode_q_split(Path) of
{'EXIT', _} -> {NewState, false};
@ -403,6 +410,7 @@ extract_path_query(#state{request_method = Method,
LQ -> LQ
end,
{NewState, {LPath, LQuery, Data}}
end
end;
extract_path_query(State) ->
{State, false}.
@ -520,7 +528,7 @@ recv_data(State, Len, Acc) ->
recv_data(State, Len - byte_size(Data), <<Acc/binary, Data/binary>>);
Err ->
?DEBUG("Cannot receive HTTP data: ~p", [Err]),
<<"">>
error
end;
_ ->
Trail = (State#state.trail),
@ -763,7 +771,8 @@ parse_auth(<<"Basic ", Auth64/binary>>) ->
undefined;
Pos ->
{User, <<$:, Pass/binary>>} = erlang:split_binary(Auth, Pos-1),
{User, Pass}
PassUtf8 = unicode:characters_to_binary(binary_to_list(Pass), utf8),
{User, PassUtf8}
end;
parse_auth(<<"Bearer ", SToken/binary>>) ->
Token = str:strip(SToken),

View File

@ -336,8 +336,9 @@ handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs,
init([Sid, Key, IP, HOpts]) ->
?DEBUG("started: ~p", [{Sid, Key, IP}]),
Opts1 = ejabberd_c2s_config:get_c2s_limits(),
SOpts = lists:filtermap(fun({stream_managment, _}) -> true;
SOpts = lists:filtermap(fun({stream_management, _}) -> true;
({max_ack_queue, _}) -> true;
({ack_timeout, _}) -> true;
({resume_timeout, _}) -> true;
({max_resume_timeout, _}) -> true;
({resend_on_timeout, _}) -> true;

View File

@ -112,8 +112,9 @@ socket_handoff(LocalPath, Request, Socket, SockMod, Buf, Opts) ->
%%% Internal
init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) ->
SOpts = lists:filtermap(fun({stream_managment, _}) -> true;
SOpts = lists:filtermap(fun({stream_management, _}) -> true;
({max_ack_queue, _}) -> true;
({ack_timeout, _}) -> true;
({resume_timeout, _}) -> true;
({max_resume_timeout, _}) -> true;
({resend_on_timeout, _}) -> true;

View File

@ -184,6 +184,8 @@ refresh_iq_handlers() ->
ejabberd_local ! refresh_iq_handlers.
-spec bounce_resource_packet(jid(), jid(), stanza()) -> ok.
bounce_resource_packet(_From, _To, #presence{}) ->
ok;
bounce_resource_packet(From, To, Packet) ->
Lang = xmpp:get_lang(Packet),
Txt = <<"No available resource found">>,
@ -282,25 +284,16 @@ do_route(From, To, Packet) ->
?DEBUG("local route~n\tfrom ~p~n\tto ~p~n\tpacket "
"~P~n",
[From, To, Packet, 8]),
Type = xmpp:get_type(Packet),
if To#jid.luser /= <<"">> ->
ejabberd_sm:route(From, To, Packet);
To#jid.lresource == <<"">> ->
case Packet of
#iq{} ->
is_record(Packet, iq), To#jid.lresource == <<"">> ->
process_iq(From, To, Packet);
#message{type = T} when T /= headline, T /= error ->
Err = xmpp:make_error(Packet, xmpp:err_service_unavailable()),
ejabberd_router:route(To, From, Err);
_ -> ok
end;
Type == result; Type == error; Type == headline ->
ok;
true ->
case xmpp:get_type(Packet) of
error -> ok;
result -> ok;
_ ->
ejabberd_hooks:run(local_send_to_resource_hook,
To#jid.lserver, [From, To, Packet])
end
end.
-spec update_table() -> ok.

View File

@ -39,16 +39,17 @@
authenticate_user/2,
authenticate_client/2,
verify_resowner_scope/3,
verify_client_scope/3,
associate_access_code/3,
associate_access_token/3,
associate_refresh_token/3,
check_token/1,
check_token/4,
check_token/2,
scope_in_scope_list/2,
process/2,
opt_type/1]).
-export([oauth_issue_token/1, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]).
-export([oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]).
-include("xmpp.hrl").
@ -57,6 +58,7 @@
-include("ejabberd_http.hrl").
-include("ejabberd_web_admin.hrl").
-include("ejabberd_oauth.hrl").
-include("ejabberd_commands.hrl").
@ -65,23 +67,30 @@
%% * Using the web form/api results in the token being generated in behalf of the user providing the user/pass
%% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin
%% (as it has access to ejabberd command line).
-record(oauth_token, {
token = {<<"">>, <<"">>} :: {binary(), binary()},
us = {<<"">>, <<"">>} :: {binary(), binary()} | server_admin,
scope = [] :: [binary()],
expire :: integer()
}).
-define(EXPIRE, 3600).
-define(EXPIRE, 4294967).
start() ->
init_db(mnesia, ?MYNAME),
DBMod = get_db_backend(),
DBMod:init(),
MaxSize =
ejabberd_config:get_option(
oauth_cache_size,
fun(I) when is_integer(I), I>0 -> I end,
1000),
LifeTime =
ejabberd_config:get_option(
oauth_cache_life_time,
fun(I) when is_integer(I), I>0 -> I end,
timer:hours(1) div 1000),
cache_tab:new(oauth_token,
[{max_size, MaxSize}, {life_time, LifeTime}]),
Expire = expire(),
application:set_env(oauth2, backend, ejabberd_oauth),
application:set_env(oauth2, expiry_time, Expire),
application:start(oauth2),
ChildSpec = {?MODULE, {?MODULE, start_link, []},
temporary, 1000, worker, [?MODULE]},
transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec),
ejabberd_commands:register_commands(get_commands_spec()),
ok.
@ -90,57 +99,63 @@ start() ->
get_commands_spec() ->
[
#ejabberd_commands{name = oauth_issue_token, tags = [oauth],
desc = "Issue an oauth token. Available scopes are the ones usable by ejabberd admins",
desc = "Issue an oauth token for the given jid",
module = ?MODULE, function = oauth_issue_token,
args = [{scopes, string}],
args = [{jid, string},{ttl, integer}, {scopes, string}],
policy = restricted,
args_example = ["connected_users_number;muc_online_rooms"],
args_desc = ["List of scopes to allow, separated by ';'"],
args_example = ["user@server.com", "connected_users_number;muc_online_rooms"],
args_desc = ["Jid for which issue token",
"Time to live of generated token in seconds",
"List of scopes to allow, separated by ';'"],
result = {result, {tuple, [{token, string}, {scopes, string}, {expires_in, string}]}}
},
#ejabberd_commands{name = oauth_list_tokens, tags = [oauth],
desc = "List oauth tokens, their scope, and how many seconds remain until expirity",
desc = "List oauth tokens, their user and scope, and how many seconds remain until expirity",
module = ?MODULE, function = oauth_list_tokens,
args = [],
policy = restricted,
result = {tokens, {list, {token, {tuple, [{token, string}, {scope, string}, {expires_in, string}]}}}}
result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}
},
#ejabberd_commands{name = oauth_list_scopes, tags = [oauth],
desc = "List scopes that can be granted to tokens generated through the command line",
desc = "List scopes that can be granted to tokens generated through the command line, together with the commands they allow",
module = ?MODULE, function = oauth_list_scopes,
args = [],
policy = restricted,
result = {scopes, {list, {scope, string}}}
result = {scopes, {list, {scope, {tuple, [{scope, string}, {commands, string}]}}}}
},
#ejabberd_commands{name = oauth_revoke_token, tags = [oauth],
desc = "Revoke authorization for a token",
module = ?MODULE, function = oauth_revoke_token,
args = [{token, string}],
policy = restricted,
result = {tokens, {list, {token, {tuple, [{token, string}, {scope, string}, {expires_in, string}]}}}},
result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}},
result_desc = "List of remaining tokens"
}
].
oauth_issue_token(ScopesString) ->
oauth_issue_token(Jid, TTLSeconds, ScopesString) ->
Scopes = [list_to_binary(Scope) || Scope <- string:tokens(ScopesString, ";")],
case oauth2:authorize_client_credentials(ejabberd_ctl, Scopes, none) of
{ok, {_AppCtx, Authorization}} ->
{ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, none),
case jid:from_string(list_to_binary(Jid)) of
#jid{luser =Username, lserver = Server} ->
case oauth2:authorize_password({Username, Server}, Scopes, admin_generated) of
{ok, {_Ctx,Authorization}} ->
{ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, [{expiry_time, TTLSeconds}]),
{ok, AccessToken} = oauth2_response:access_token(Response),
{ok, Expires} = oauth2_response:expires_in(Response),
{ok, VerifiedScope} = oauth2_response:scope(Response),
{AccessToken, VerifiedScope, integer_to_list(Expires) ++ " seconds"};
{AccessToken, VerifiedScope, integer_to_list(TTLSeconds) ++ " seconds"};
{error, Error} ->
{error, Error}
end;
error ->
{error, "Invalid JID: " ++ Jid}
end.
oauth_list_tokens() ->
Tokens = mnesia:dirty_match_object(#oauth_token{us = server_admin, _ = '_'}),
Tokens = mnesia:dirty_match_object(#oauth_token{_ = '_'}),
{MegaSecs, Secs, _MiniSecs} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
[{Token, Scope, integer_to_list(Expires - TS) ++ " seconds"} ||
#oauth_token{token=Token, scope=Scope, expire=Expires} <- Tokens].
[{Token, jid:to_string(jid:make(U,S,<<>>)), Scope, integer_to_list(Expires - TS) ++ " seconds"} ||
#oauth_token{token=Token, scope=Scope, us= {U,S},expire=Expires} <- Tokens].
oauth_revoke_token(Token) ->
@ -148,8 +163,7 @@ oauth_revoke_token(Token) ->
oauth_list_tokens().
oauth_list_scopes() ->
get_cmd_scopes().
[ {Scope, string:join([atom_to_list(Cmd) || Cmd <- Cmds], ",")} || {Scope, Cmds} <- dict:to_list(get_cmd_scopes())].
@ -170,15 +184,8 @@ handle_cast(_Msg, State) -> {noreply, State}.
handle_info(clean, State) ->
{MegaSecs, Secs, MiniSecs} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
F = fun() ->
Ts = mnesia:select(
oauth_token,
[{#oauth_token{expire = '$1', _ = '_'},
[{'<', '$1', TS}],
['$_']}]),
lists:foreach(fun mnesia:delete_object/1, Ts)
end,
mnesia:async_dirty(F),
DBMod = get_db_backend(),
DBMod:clean(TS),
erlang:send_after(trunc(expire() * 1000 * (1 + MiniSecs / 1000000)),
self(), clean),
{noreply, State};
@ -189,21 +196,11 @@ terminate(_Reason, _State) -> ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
init_db(mnesia, _Host) ->
mnesia:create_table(oauth_token,
[{disc_copies, [node()]},
{attributes,
record_info(fields, oauth_token)}]),
mnesia:add_table_copy(oauth_token, node(), disc_copies);
init_db(_, _) ->
ok.
get_client_identity(Client, Ctx) -> {ok, {Ctx, {client, Client}}}.
verify_redirection_uri(_, _, Ctx) -> {ok, Ctx}.
authenticate_user({User, Server}, {password, Password} = Ctx) ->
authenticate_user({User, Server}, Ctx) ->
case jid:make(User, Server, <<"">>) of
#jid{} = JID ->
Access =
@ -213,12 +210,17 @@ authenticate_user({User, Server}, {password, Password} = Ctx) ->
none),
case acl:match_rule(JID#jid.lserver, Access, JID) of
allow ->
case Ctx of
{password, Password} ->
case ejabberd_auth:check_password(User, <<"">>, Server, Password) of
true ->
{ok, {Ctx, {user, User, Server}}};
false ->
{error, badpass}
end;
admin_generated ->
{ok, {Ctx, {user, User, Server}}}
end;
deny ->
{error, badpass}
end;
@ -229,8 +231,8 @@ authenticate_user({User, Server}, {password, Password} = Ctx) ->
authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}.
verify_resowner_scope({user, _User, _Server}, Scope, Ctx) ->
Cmds = ejabberd_commands:get_commands(),
Cmds1 = [sasl_auth | Cmds],
Cmds = ejabberd_commands:get_exposed_commands(),
Cmds1 = ['ejabberd:user', 'ejabberd:admin', sasl_auth | Cmds],
RegisteredScope = [atom_to_binary(C, utf8) || C <- Cmds1],
case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
oauth2_priv_set:new(RegisteredScope)) of
@ -244,93 +246,156 @@ verify_resowner_scope(_, _, _) ->
get_cmd_scopes() ->
Cmds = lists:filter(fun(Cmd) -> case ejabberd_commands:get_command_policy(Cmd) of
{ok, Policy} when Policy =/= restricted -> true;
_ -> false
end end,
ejabberd_commands:get_commands()),
[atom_to_binary(C, utf8) || C <- Cmds].
ScopeMap = lists:foldl(fun(Cmd, Accum) ->
case ejabberd_commands:get_command_policy_and_scope(Cmd) of
{ok, Policy, Scopes} when Policy =/= restricted ->
lists:foldl(fun(Scope, Accum2) ->
dict:append(Scope, Cmd, Accum2)
end, Accum, Scopes);
_ -> Accum
end end, dict:new(), ejabberd_commands:get_exposed_commands()),
ScopeMap.
%% This is callback for oauth tokens generated through the command line. Only open and admin commands are
%% made available.
verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
RegisteredScope = get_cmd_scopes(),
case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
oauth2_priv_set:new(RegisteredScope)) of
true ->
{ok, {Ctx, Scope}};
false ->
{error, badscope}
end.
%verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
% RegisteredScope = dict:fetch_keys(get_cmd_scopes()),
% case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
% oauth2_priv_set:new(RegisteredScope)) of
% true ->
% {ok, {Ctx, Scope}};
% false ->
% {error, badscope}
% end.
-spec seconds_since_epoch(integer()) -> non_neg_integer().
seconds_since_epoch(Diff) ->
{Mega, Secs, _} = os:timestamp(),
Mega * 1000000 + Secs + Diff.
associate_access_code(_AccessCode, _Context, AppContext) ->
%put(?ACCESS_CODE_TABLE, AccessCode, Context),
{ok, AppContext}.
associate_access_token(AccessToken, Context, AppContext) ->
%% Tokens generated using the API/WEB belongs to users and always include the user, server pair.
%% Tokens generated form command line aren't tied to an user, and instead belongs to the ejabberd sysadmin
US = case proplists:get_value(<<"resource_owner">>, Context, <<"">>) of
{user, User, Server} -> {jid:nodeprep(User), jid:nodeprep(Server)};
undefined -> server_admin
{user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>),
Expire = case proplists:get_value(expiry_time, AppContext, undefined) of
undefined ->
proplists:get_value(<<"expiry_time">>, Context, 0);
ExpiresIn ->
%% There is no clean way in oauth2 lib to actually override the TTL of the generated token.
%% It always pass the global configured value. Here we use the app context to pass the per-case
%% ttl if we want to override it.
seconds_since_epoch(ExpiresIn)
end,
{user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>),
Scope = proplists:get_value(<<"scope">>, Context, []),
Expire = proplists:get_value(<<"expiry_time">>, Context, 0),
R = #oauth_token{
token = AccessToken,
us = US,
us = {jid:nodeprep(User), jid:nodeprep(Server)},
scope = Scope,
expire = Expire
},
mnesia:dirty_write(R),
store(R),
{ok, AppContext}.
associate_refresh_token(_RefreshToken, _Context, AppContext) ->
%put(?REFRESH_TOKEN_TABLE, RefreshToken, Context),
{ok, AppContext}.
scope_in_scope_list(Scope, ScopeList) ->
TokenScopeSet = oauth2_priv_set:new(Scope),
lists:any(fun(Scope2) ->
oauth2_priv_set:is_member(Scope2, TokenScopeSet) end,
ScopeList).
check_token(User, Server, Scope, Token) ->
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 catch mnesia:dirty_read(oauth_token, Token) of
[#oauth_token{us = {LUser, LServer},
case lookup(Token) of
{ok, #oauth_token{us = {LUser, LServer},
scope = TokenScope,
expire = Expire}] ->
expire = Expire}} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
oauth2_priv_set:is_member(
Scope, oauth2_priv_set:new(TokenScope)) andalso
Expire > TS;
if
Expire > TS ->
TokenScopeSet = oauth2_priv_set:new(TokenScope),
lists:any(fun(Scope) ->
oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
ScopeList);
true ->
{false, expired}
end;
_ ->
false
{false, not_found}
end.
check_token(Scope, Token) ->
case catch mnesia:dirty_read(oauth_token, Token) of
[#oauth_token{us = US,
check_token(ScopeList, Token) ->
case lookup(Token) of
{ok, #oauth_token{us = US,
scope = TokenScope,
expire = Expire}] ->
expire = Expire}} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
case oauth2_priv_set:is_member(
Scope, oauth2_priv_set:new(TokenScope)) andalso
Expire > TS of
true -> case US of
{LUser, LServer} -> {ok, user, {LUser, LServer}};
server_admin -> {ok, server_admin}
if
Expire > TS ->
TokenScopeSet = oauth2_priv_set:new(TokenScope),
case lists:any(fun(Scope) ->
oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
ScopeList) of
true -> {ok, user, US};
false -> {false, no_matching_scope}
end;
false -> false
true ->
{false, expired}
end;
_ ->
false
{false, not_found}
end.
store(R) ->
cache_tab:insert(
oauth_token, R#oauth_token.token, R,
fun() ->
DBMod = get_db_backend(),
DBMod:store(R)
end).
lookup(Token) ->
cache_tab:lookup(
oauth_token, Token,
fun() ->
DBMod = get_db_backend(),
case DBMod:lookup(Token) of
#oauth_token{} = R -> {ok, R};
_ -> error
end
end).
expire() ->
ejabberd_config:get_option(
oauth_expire,
@ -358,12 +423,9 @@ process(_Handlers,
?XAE(<<"form">>,
[{<<"action">>, <<"authorization_token">>},
{<<"method">>, <<"post">>}],
[?LABEL(<<"username">>, [?CT(<<"User">>), ?C(<<": ">>)]),
[?LABEL(<<"username">>, [?CT(<<"User (jid)">>), ?C(<<": ">>)]),
?INPUTID(<<"text">>, <<"username">>, <<"">>),
?BR,
?LABEL(<<"server">>, [?CT(<<"Server">>), ?C(<<": ">>)]),
?INPUTID(<<"text">>, <<"server">>, <<"">>),
?BR,
?LABEL(<<"password">>, [?CT(<<"Password">>), ?C(<<": ">>)]),
?INPUTID(<<"password">>, <<"password">>, <<"">>),
?INPUT(<<"hidden">>, <<"response_type">>, ResponseType),
@ -372,6 +434,15 @@ process(_Handlers,
?INPUT(<<"hidden">>, <<"scope">>, Scope),
?INPUT(<<"hidden">>, <<"state">>, State),
?BR,
?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?CT(<<": ">>)]),
?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}],
[
?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>),
?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>),
?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>),
?XAC(<<"option">>, [{<<"selected">>, <<"selected">>},{<<"value">>, <<"31536000">>}],<<"1 Year">>),
?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]),
?BR,
?INPUTT(<<"submit">>, <<"">>, <<"Accept">>)
]),
Top =
@ -415,11 +486,16 @@ process(_Handlers,
ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>),
RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>),
SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
Username = proplists:get_value(<<"username">>, Q, <<"">>),
Server = proplists:get_value(<<"server">>, Q, <<"">>),
StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
#jid{user = Username, server = Server} = jid:from_string(StringJID),
Password = proplists:get_value(<<"password">>, Q, <<"">>),
State = proplists:get_value(<<"state">>, Q, <<"">>),
Scope = str:tokens(SScope, <<" ">>),
TTL = proplists:get_value(<<"ttl">>, Q, <<"">>),
ExpiresIn = case TTL of
<<>> -> undefined;
_ -> jlib:binary_to_integer(TTL)
end,
case oauth2:authorize_password({Username, Server},
ClientId,
RedirectURI,
@ -427,10 +503,18 @@ process(_Handlers,
{password, Password}) of
{ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} =
oauth2:issue_token(Authorization, none),
oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
{ok, AccessToken} = oauth2_response:access_token(Response),
{ok, Type} = oauth2_response:token_type(Response),
{ok, Expires} = oauth2_response:expires_in(Response),
%%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have
%%per-case expirity time.
Expires = case ExpiresIn of
undefined ->
{ok, Ex} = oauth2_response:expires_in(Response),
Ex;
_ ->
ExpiresIn
end,
{ok, VerifiedScope} = oauth2_response:scope(Response),
%oauth2_wrq:redirected_access_token_response(ReqData,
% RedirectURI,
@ -459,11 +543,82 @@ process(_Handlers,
}],
ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])}
end;
process(_Handlers,
#request{method = 'POST', q = Q, lang = _Lang,
path = [_, <<"token">>]}) ->
case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
<<"password">> ->
SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
#jid{user = Username, server = Server} = jid:from_string(StringJID),
Password = proplists:get_value(<<"password">>, Q, <<"">>),
Scope = str:tokens(SScope, <<" ">>),
TTL = proplists:get_value(<<"ttl">>, Q, <<"">>),
ExpiresIn = case TTL of
<<>> -> undefined;
_ -> jlib:binary_to_integer(TTL)
end,
case oauth2:authorize_password({Username, Server},
Scope,
{password, Password}) of
{ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} =
oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
{ok, AccessToken} = oauth2_response:access_token(Response),
{ok, Type} = oauth2_response:token_type(Response),
%%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have
%%per-case expirity time.
Expires = case ExpiresIn of
undefined ->
{ok, Ex} = oauth2_response:expires_in(Response),
Ex;
_ ->
ExpiresIn
end,
{ok, VerifiedScope} = oauth2_response:scope(Response),
json_response(200, {[
{<<"access_token">>, AccessToken},
{<<"token_type">>, Type},
{<<"scope">>, str:join(VerifiedScope, <<" ">>)},
{<<"expires_in">>, Expires}]});
{error, Error} when is_atom(Error) ->
json_error(400, <<"invalid_grant">>, Error)
end;
_OtherGrantType ->
json_error(400, <<"unsupported_grant_type">>, unsupported_grant_type)
end;
process(_Handlers, _Request) ->
ejabberd_web:error(not_found).
-spec get_db_backend() -> module().
get_db_backend() ->
DBType = ejabberd_config:get_option(
oauth_db_type,
fun(T) -> ejabberd_config:v_db(?MODULE, T) end,
mnesia),
list_to_atom("ejabberd_oauth_" ++ atom_to_list(DBType)).
%% Headers as per RFC 6749
json_response(Code, Body) ->
{Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>},
{<<"Cache-Control">>, <<"no-store">>},
{<<"Pragma">>, <<"no-cache">>}],
jiffy:encode(Body)}.
%% OAauth error are defined in:
%% https://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-5.2
json_error(Code, Error, Reason) ->
Desc = json_error_desc(Reason),
Body = {[{<<"error">>, Error},
{<<"error_description">>, Desc}]},
json_response(Code, Body).
json_error_desc(access_denied) -> <<"Access denied">>;
json_error_desc(unsupported_grant_type) -> <<"Unsupported grant type">>;
json_error_desc(invalid_scope) -> <<"Invalid scope">>.
web_head() ->
[?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>},
@ -595,4 +750,10 @@ opt_type(oauth_expire) ->
fun(I) when is_integer(I), I >= 0 -> I end;
opt_type(oauth_access) ->
fun acl:access_rules_validator/1;
opt_type(_) -> [oauth_expire, oauth_access].
opt_type(oauth_db_type) ->
fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
opt_type(oauth_cache_life_time) ->
fun (I) when is_integer(I), I > 0 -> I end;
opt_type(oauth_cache_size) ->
fun (I) when is_integer(I), I > 0 -> I end;
opt_type(_) -> [oauth_expire, oauth_access, oauth_db_type].

View File

@ -0,0 +1,65 @@
%%%-------------------------------------------------------------------
%%% File : ejabberd_oauth_mnesia.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : OAUTH2 mnesia backend
%%% Created : 20 Jul 2016 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License
%%% along with this program; if not, write to the Free Software
%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
%%% 02111-1307 USA
%%%
%%%-------------------------------------------------------------------
-module(ejabberd_oauth_mnesia).
-export([init/0,
store/1,
lookup/1,
clean/1]).
-include("ejabberd_oauth.hrl").
init() ->
mnesia:create_table(oauth_token,
[{disc_copies, [node()]},
{attributes,
record_info(fields, oauth_token)}]),
mnesia:add_table_copy(oauth_token, node(), disc_copies),
ok.
store(R) ->
mnesia:dirty_write(R).
lookup(Token) ->
case catch mnesia:dirty_read(oauth_token, Token) of
[R] ->
R;
_ ->
false
end.
clean(TS) ->
F = fun() ->
Ts = mnesia:select(
oauth_token,
[{#oauth_token{expire = '$1', _ = '_'},
[{'<', '$1', TS}],
['$_']}]),
lists:foreach(fun mnesia:delete_object/1, Ts)
end,
mnesia:async_dirty(F).

View File

@ -0,0 +1,98 @@
%%%-------------------------------------------------------------------
%%% File : ejabberd_oauth_rest.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : OAUTH2 REST backend
%%% Created : 26 Jul 2016 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License
%%% along with this program; if not, write to the Free Software
%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
%%% 02111-1307 USA
%%%
%%%-------------------------------------------------------------------
-module(ejabberd_oauth_rest).
-export([init/0,
store/1,
lookup/1,
clean/1,
opt_type/1]).
-include("ejabberd.hrl").
-include("ejabberd_oauth.hrl").
-include("logger.hrl").
-include("jlib.hrl").
init() ->
rest:start(?MYNAME),
ok.
store(R) ->
Path = path(<<"store">>),
%% Retry 2 times, with a backoff of 500millisec
{User, Server} = R#oauth_token.us,
SJID = jid:to_string({User, Server, <<"">>}),
case rest:with_retry(
post,
[?MYNAME, Path, [],
{[{<<"token">>, R#oauth_token.token},
{<<"user">>, SJID},
{<<"scope">>, R#oauth_token.scope},
{<<"expire">>, R#oauth_token.expire}
]}], 2, 500) of
{ok, Code, _} when Code == 200 orelse Code == 201 ->
ok;
Err ->
?ERROR_MSG("failed to store oauth record ~p: ~p", [R, Err]),
{error, Err}
end.
lookup(Token) ->
Path = path(<<"lookup">>),
case rest:with_retry(post, [?MYNAME, Path, [],
{[{<<"token">>, Token}]}],
2, 500) of
{ok, 200, {Data}} ->
SJID = proplists:get_value(<<"user">>, Data, <<>>),
JID = jid:from_string(SJID),
US = {JID#jid.luser, JID#jid.lserver},
Scope = proplists:get_value(<<"scope">>, Data, []),
Expire = proplists:get_value(<<"expire">>, Data, 0),
#oauth_token{token = Token,
us = US,
scope = Scope,
expire = Expire};
{ok, 404, _Resp} ->
false;
Other ->
?ERROR_MSG("Unexpected response for oauth lookup: ~p", [Other]),
{error, rest_failed}
end.
clean(_TS) ->
ok.
path(Path) ->
Base = ejabberd_config:get_option(ext_api_path_oauth,
fun(X) -> iolist_to_binary(X) end,
<<"/oauth">>),
<<Base/binary, "/", Path/binary>>.
opt_type(ext_api_path_oauth) ->
fun (X) -> iolist_to_binary(X) end;
opt_type(_) -> [ext_api_path_oauth].

View File

@ -0,0 +1,78 @@
%%%-------------------------------------------------------------------
%%% File : ejabberd_oauth_sql.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : OAUTH2 SQL backend
%%% Created : 27 Jul 2016 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License
%%% along with this program; if not, write to the Free Software
%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
%%% 02111-1307 USA
%%%
%%%-------------------------------------------------------------------
-module(ejabberd_oauth_sql).
-compile([{parse_transform, ejabberd_sql_pt}]).
-export([init/0,
store/1,
lookup/1,
clean/1]).
-include("ejabberd_oauth.hrl").
-include("ejabberd.hrl").
-include("ejabberd_sql_pt.hrl").
-include("jlib.hrl").
init() ->
ok.
store(R) ->
Token = R#oauth_token.token,
{User, Server} = R#oauth_token.us,
SJID = jid:to_string({User, Server, <<"">>}),
Scope = str:join(R#oauth_token.scope, <<" ">>),
Expire = R#oauth_token.expire,
?SQL_UPSERT(
?MYNAME,
"oauth_token",
["!token=%(Token)s",
"jid=%(SJID)s",
"scope=%(Scope)s",
"expire=%(Expire)d"]).
lookup(Token) ->
case ejabberd_sql:sql_query(
?MYNAME,
?SQL("select @(jid)s, @(scope)s, @(expire)d"
" from oauth_token where token=%(Token)s")) of
{selected, [{SJID, Scope, Expire}]} ->
JID = jid:from_string(SJID),
US = {JID#jid.luser, JID#jid.lserver},
#oauth_token{token = Token,
us = US,
scope = str:tokens(Scope, <<" ">>),
expire = Expire};
_ ->
false
end.
clean(TS) ->
ejabberd_sql:sql_query(
?MYNAME,
?SQL("delete from oauth_token where expire < %(TS)d")).

View File

@ -466,19 +466,17 @@ send_element(Pid, El) ->
%%% ejabberd commands
get_commands_spec() ->
[#ejabberd_commands{name = incoming_s2s_number,
[#ejabberd_commands{
name = incoming_s2s_number,
tags = [stats, s2s],
desc =
"Number of incoming s2s connections on "
"the node",
desc = "Number of incoming s2s connections on the node",
policy = admin,
module = ?MODULE, function = incoming_s2s_number,
args = [], result = {s2s_incoming, integer}},
#ejabberd_commands{name = outgoing_s2s_number,
#ejabberd_commands{
name = outgoing_s2s_number,
tags = [stats, s2s],
desc =
"Number of outgoing s2s connections on "
"the node",
desc = "Number of outgoing s2s connections on the node",
policy = admin,
module = ?MODULE, function = outgoing_s2s_number,
args = [], result = {s2s_outgoing, integer}},
@ -489,11 +487,19 @@ get_commands_spec() ->
module = ?MODULE, function = stop_all_connections,
args = [], result = {res, rescode}}].
%% TODO Move those stats commands to ejabberd stats command ?
incoming_s2s_number() ->
length(supervisor:which_children(ejabberd_s2s_in_sup)).
supervisor_count(ejabberd_s2s_in_sup).
outgoing_s2s_number() ->
length(supervisor:which_children(ejabberd_s2s_out_sup)).
supervisor_count(ejabberd_s2s_out_sup).
supervisor_count(Supervisor) ->
case catch supervisor:which_children(Supervisor) of
{'EXIT', _} -> 0;
Result ->
length(Result)
end.
stop_all_connections() ->
lists:foreach(

View File

@ -850,13 +850,12 @@ get_addr_port(Server) ->
?DEBUG("srv lookup of '~s': ~p~n",
[Server, HEnt#hostent.h_addr_list]),
AddrList = HEnt#hostent.h_addr_list,
random:seed(p1_time_compat:timestamp()),
case catch lists:map(fun ({Priority, Weight, Port,
Host}) ->
N = case Weight of
0 -> 0;
_ ->
(Weight + 1) * random:uniform()
(Weight + 1) * randoms:uniform()
end,
{Priority * 65536 - N, Host, Port}
end,

View File

@ -98,13 +98,13 @@ init([{SockMod, Socket}, Opts]) ->
fun({H, Os}, D) ->
P = proplists:get_value(
password, Os,
p1_sha:sha(crypto:rand_bytes(20))),
p1_sha:sha(randoms:bytes(20))),
dict:store(H, P, D)
end, dict:new(), HOpts);
false ->
Pass = proplists:get_value(
password, Opts,
p1_sha:sha(crypto:rand_bytes(20))),
p1_sha:sha(randoms:bytes(20))),
dict:from_list([{global, Pass}])
end,
Shaper = case lists:keysearch(shaper_rule, 1, Opts) of
@ -170,27 +170,37 @@ wait_for_stream(closed, StateData) ->
wait_for_handshake({xmlstreamelement, El}, StateData) ->
decode_element(El, wait_for_handshake, StateData);
wait_for_handshake(#handshake{data = Digest}, StateData) ->
case dict:find(StateData#state.host, StateData#state.host_opts) of
{ok, Password} ->
case p1_sha:sha(<<(StateData#state.streamid)/binary,
Password/binary>>) of
Digest ->
send_element(StateData, #handshake{}),
lists:foreach(
fun (H) ->
ejabberd_router:register_route(H, ?MYNAME),
?INFO_MSG("Route registered for service ~p~n",
[H])
[H]),
ejabberd_hooks:run(component_connected, [H])
end, dict:fetch_keys(StateData#state.host_opts)),
{next_state, stream_established, StateData};
_ ->
send_element(StateData, xmpp:serr_not_authorized()),
{stop, normal, StateData}
end;
_ ->
send_element(StateData, xmpp:serr_not_authorized()),
{stop, normal, StateData}
end;
%% case dict:find(StateData#state.host, StateData#state.host_opts) of
%% {ok, Password} ->
%% case p1_sha:sha(<<(StateData#state.streamid)/binary,
%% Password/binary>>) of
%% Digest ->
%% send_element(StateData, #handshake{}),
%% lists:foreach(
%% fun (H) ->
%% ejabberd_router:register_route(H, ?MYNAME),
%% ?INFO_MSG("Route registered for service ~p~n",
%% [H]),
%% ejabberd_hooks:run(component_connected, [H])
%% end, dict:fetch_keys(StateData#state.host_opts)),
%% {next_state, stream_established, StateData};
%% _ ->
%% send_element(StateData, xmpp:serr_not_authorized()),
%% {stop, normal, StateData}
%% end;
%% _ ->
%% send_element(StateData, xmpp:serr_not_authorized()),
%% {stop, normal, StateData}
%% end;
wait_for_handshake({xmlstreamend, _Name}, StateData) ->
{stop, normal, StateData};
wait_for_handshake({xmlstreamerror, _}, StateData) ->
@ -211,24 +221,10 @@ stream_established(El, StateData) when ?is_stanza(El) ->
Txt = <<"Missing 'from' or 'to' attribute">>,
send_error(StateData, El, xmpp:err_jid_malformed(Txt, Lang));
true ->
FromJID = case StateData#state.check_from of
false ->
%% If the admin does not want to check the from field
%% when accept packets from any address.
%% In this case, the component can send packet of
%% behalf of the server users.
From;
_ ->
%% The default is the standard behaviour in XEP-0114
Server = From#jid.lserver,
case dict:is_key(Server, StateData#state.host_opts) of
true -> From;
false -> error
end
end,
if FromJID /= error ->
ejabberd_router:route(FromJID, To, El);
case check_from(From, StateData) of
true ->
ejabberd_router:route(From, To, El);
false ->
Txt = <<"Improper domain part of 'from' attribute">>,
send_error(StateData, El, xmpp:err_not_allowed(Txt, Lang))
end
@ -281,7 +277,9 @@ terminate(Reason, StateName, StateData) ->
case StateName of
stream_established ->
lists:foreach(fun (H) ->
ejabberd_router:unregister_route(H)
ejabberd_router:unregister_route(H),
ejabberd_hooks:run(component_disconnected,
[H, Reason])
end,
dict:fetch_keys(StateData#state.host_opts));
_ -> ok
@ -350,6 +348,18 @@ decode_element(#xmlel{} = El, StateName, StateData) ->
{next_state, StateName, StateData}
end.
-spec check_from(jid(), state()) -> boolean().
check_from(_From, #state{check_from = false}) ->
%% If the admin does not want to check the from field
%% when accept packets from any address.
%% In this case, the component can send packet of
%% behalf of the server users.
true;
check_from(From, StateData) ->
%% The default is the standard behaviour in XEP-0114
Server = From#jid.lserver,
dict:is_key(Server, StateData#state.host_opts).
-spec new_id() -> binary().
new_id() -> randoms:get_string().

View File

@ -279,25 +279,28 @@ get_session_pid(User, Server, Resource) ->
-spec set_offline_info(sid(), binary(), binary(), binary(), info()) -> ok.
set_offline_info({Time, _Pid}, User, Server, Resource, Info) ->
SID = {Time, undefined},
set_offline_info(SID, User, Server, Resource, Info) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
LResource = jid:resourceprep(Resource),
set_session(SID, LUser, LServer, LResource, undefined, Info).
set_session(SID, LUser, LServer, LResource, undefined, [offline | Info]).
-spec get_offline_info(erlang:timestamp(), binary(), binary(),
binary()) -> none | info().
get_offline_info(Time, User, Server, Resource) ->
SID = {Time, undefined},
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
LResource = jid:resourceprep(Resource),
Mod = get_sm_backend(LServer),
case Mod:get_sessions(LUser, LServer, LResource) of
[#session{sid = SID, info = Info}] ->
[#session{sid = {Time, _}, info = Info}] ->
case proplists:get_bool(offline, Info) of
true ->
Info;
false ->
none
end;
_ ->
none
end.
@ -434,11 +437,12 @@ set_session(SID, User, Server, Resource, Priority, Info) ->
-spec online([#session{}]) -> [#session{}].
online(Sessions) ->
lists:filter(fun(#session{sid = {_, undefined}}) ->
false;
(_) ->
true
end, Sessions).
lists:filter(fun is_online/1, Sessions).
-spec is_online(#session{}) -> boolean().
is_online(#session{info = Info}) ->
not proplists:get_bool(offline, Info).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec do_route(jid(), jid(), stanza() | broadcast()) -> any().
@ -629,15 +633,17 @@ check_for_sessions_to_replace(User, Server, Resource) ->
-spec check_existing_resources(binary(), binary(), binary()) -> ok.
check_existing_resources(LUser, LServer, LResource) ->
SIDs = get_resource_sessions(LUser, LServer, LResource),
if SIDs == [] -> ok;
true ->
MaxSID = lists:max(SIDs),
lists:foreach(fun ({_, undefined} = S) ->
Mod = get_sm_backend(LServer),
Mod:delete_session(LUser, LServer, LResource,
S);
({_, Pid} = S) when S /= MaxSID ->
Ss = Mod:get_sessions(LUser, LServer, LResource),
{OnlineSs, OfflineSs} = lists:partition(fun is_online/1, Ss),
lists:foreach(fun(#session{sid = S}) ->
Mod:delete_session(LUser, LServer, LResource, S)
end, OfflineSs),
if OnlineSs == [] -> ok;
true ->
SIDs = [SID || #session{sid = SID} <- OnlineSs],
MaxSID = lists:max(SIDs),
lists:foreach(fun ({_, Pid} = S) when S /= MaxSID ->
Pid ! replaced;
(_) -> ok
end,
@ -660,10 +666,18 @@ get_resource_sessions(User, Server, Resource) ->
-spec check_max_sessions(binary(), binary()) -> ok | replaced.
check_max_sessions(LUser, LServer) ->
Mod = get_sm_backend(LServer),
SIDs = [S#session.sid || S <- online(Mod:get_sessions(LUser, LServer))],
Ss = Mod:get_sessions(LUser, LServer),
{OnlineSs, OfflineSs} = lists:partition(fun is_online/1, Ss),
MaxSessions = get_max_user_sessions(LUser, LServer),
if length(SIDs) =< MaxSessions -> ok;
true -> {_, Pid} = lists:min(SIDs), Pid ! replaced
if length(OnlineSs) =< MaxSessions -> ok;
true ->
#session{sid = {_, Pid}} = lists:min(OnlineSs),
Pid ! replaced
end,
if length(OfflineSs) =< MaxSessions -> ok;
true ->
#session{sid = SID, usr = {_, _, R}} = lists:min(OfflineSs),
Mod:delete_session(LUser, LServer, R, SID)
end.
%% Get the user_max_session setting

View File

@ -144,7 +144,10 @@ clean_table() ->
{_, SID} = binary_to_term(USSIDKey),
node(element(2, SID)) == node()
end, Vals),
Q1 = ["HDEL", ServKey | Vals1],
Q1 = case Vals1 of
[] -> [];
_ -> ["HDEL", ServKey | Vals1]
end,
Q2 = lists:map(
fun(USSIDKey) ->
{US, SID} = binary_to_term(USSIDKey),
@ -152,7 +155,7 @@ clean_table() ->
SIDKey = sid_to_key(SID),
["HDEL", USKey, SIDKey]
end, Vals1),
Res = ejabberd_redis:qp([Q1|Q2]),
Res = ejabberd_redis:qp(lists:delete([], [Q1|Q2])),
case lists:filter(
fun({ok, _}) -> false;
(_) -> true

View File

@ -629,7 +629,7 @@ generic_sql_query_format(SQLQuery) ->
generic_escape() ->
#sql_escape{string = fun(X) -> <<"'", (escape(X))/binary, "'">> end,
integer = fun(X) -> integer_to_binary(X) end,
integer = fun(X) -> jlib:i2l(X) end,
boolean = fun(true) -> <<"1">>;
(false) -> <<"0">>
end
@ -646,7 +646,7 @@ sqlite_sql_query_format(SQLQuery) ->
sqlite_escape() ->
#sql_escape{string = fun(X) -> <<"'", (standard_escape(X))/binary, "'">> end,
integer = fun(X) -> integer_to_binary(X) end,
integer = fun(X) -> jlib:i2l(X) end,
boolean = fun(true) -> <<"1">>;
(false) -> <<"0">>
end
@ -670,7 +670,7 @@ pgsql_prepare(SQLQuery, State) ->
pgsql_execute_escape() ->
#sql_escape{string = fun(X) -> X end,
integer = fun(X) -> [integer_to_binary(X)] end,
integer = fun(X) -> [jlib:i2l(X)] end,
boolean = fun(true) -> "1";
(false) -> "0"
end
@ -790,7 +790,7 @@ pgsql_connect(Server, Port, DB, Username, Password) ->
{port, Port},
{as_binary, true}]) of
{ok, Ref} ->
pgsql:squery(Ref, [<<"alter database ">>, DB, <<" set ">>,
pgsql:squery(Ref, [<<"alter database \"">>, DB, <<"\" set ">>,
<<"standard_conforming_strings='off';">>]),
pgsql:squery(Ref, [<<"set standard_conforming_strings to 'off';">>]),
{ok, Ref};

View File

@ -74,20 +74,27 @@ get_acl_rule([<<"vhosts">>], _) ->
%% The pages of a vhost are only accesible if the user is admin of that vhost:
get_acl_rule([<<"server">>, VHost | _RPath], Method)
when Method =:= 'GET' orelse Method =:= 'HEAD' ->
{VHost, [configure, webadmin_view]};
AC = gen_mod:get_module_opt(VHost, ejabberd_web_admin,
access, fun(A) -> A end, configure),
ACR = gen_mod:get_module_opt(VHost, ejabberd_web_admin,
access_readonly, fun(A) -> A end, webadmin_view),
{VHost, [AC, ACR]};
get_acl_rule([<<"server">>, VHost | _RPath], 'POST') ->
{VHost, [configure]};
AC = gen_mod:get_module_opt(VHost, ejabberd_web_admin,
access, fun(A) -> A end, configure),
{VHost, [AC]};
%% Default rule: only global admins can access any other random page
get_acl_rule(_RPath, Method)
when Method =:= 'GET' orelse Method =:= 'HEAD' ->
{global, [configure, webadmin_view]};
get_acl_rule(_RPath, 'POST') -> {global, [configure]}.
is_acl_match(Host, Rules, Jid) ->
lists:any(fun (Rule) ->
allow == acl:match_rule(Host, Rule, Jid)
end,
Rules).
AC = gen_mod:get_module_opt(global, ejabberd_web_admin,
access, fun(A) -> A end, configure),
ACR = gen_mod:get_module_opt(global, ejabberd_web_admin,
access_readonly, fun(A) -> A end, webadmin_view),
{global, [AC, ACR]};
get_acl_rule(_RPath, 'POST') ->
AC = gen_mod:get_module_opt(global, ejabberd_web_admin,
access, fun(A) -> A end, configure),
{global, [AC]}.
%%%==================================
%%%% Menu Items Access
@ -138,7 +145,7 @@ is_allowed_path([<<"admin">> | Path], JID) ->
is_allowed_path(Path, JID);
is_allowed_path(Path, JID) ->
{HostOfRule, AccessRule} = get_acl_rule(Path, 'GET'),
is_acl_match(HostOfRule, AccessRule, JID).
acl:any_rules_allowed(HostOfRule, AccessRule, JID).
%% @spec(Path) -> URL
%% where Path = [string()]
@ -266,7 +273,7 @@ get_auth_account(HostOfRule, AccessRule, User, Server,
Pass) ->
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
true ->
case is_acl_match(HostOfRule, AccessRule,
case acl:any_rules_allowed(HostOfRule, AccessRule,
jid:make(User, Server, <<"">>))
of
false -> {unauthorized, <<"unprivileged-account">>};
@ -740,7 +747,7 @@ process_admin(Host,
_ -> nothing
end,
ACLs = lists:keysort(2,
ets:select(acl,
mnesia:dirty_select(acl,
[{{acl, {'$1', Host}, '$2'}, [],
[{{acl, '$1', '$2'}}]}])),
{NumLines, ACLsP} = term_to_paragraph(ACLs, 80),
@ -777,7 +784,7 @@ process_admin(Host,
_ -> nothing
end,
ACLs = lists:keysort(2,
ets:select(acl,
mnesia:dirty_select(acl,
[{{acl, {'$1', Host}, '$2'}, [],
[{{acl, '$1', '$2'}}]}])),
make_xhtml((?H1GL((?T(<<"Access Control Lists">>)),
@ -842,7 +849,7 @@ process_admin(Host,
end;
_ -> nothing
end,
Access = ets:select(access,
Access = mnesia:dirty_select(access,
[{{access, {'$1', Host}, '$2'}, [],
[{{access, '$1', '$2'}}]}]),
{NumLines, AccessP} = term_to_paragraph(lists:keysort(2,Access), 80),
@ -876,7 +883,7 @@ process_admin(Host,
end;
_ -> nothing
end,
AccessRules = ets:select(access,
AccessRules = mnesia:dirty_select(access,
[{{access, {'$1', Host}, '$2'}, [],
[{{access, '$1', '$2'}}]}]),
make_xhtml((?H1GL((?T(<<"Access Rules">>)),
@ -1150,7 +1157,7 @@ term_to_paragraph(T, Cols) ->
term_to_id(T) -> jlib:encode_base64((term_to_binary(T))).
acl_parse_query(Host, Query) ->
ACLs = ets:select(acl,
ACLs = mnesia:dirty_select(acl,
[{{acl, {'$1', Host}, '$2'}, [],
[{{acl, '$1', '$2'}}]}]),
case lists:keysearch(<<"submit">>, 1, Query) of
@ -1264,7 +1271,7 @@ access_rules_to_xhtml(AccessRules, Lang) ->
<<"Add New">>)])])]))]).
access_parse_query(Host, Query) ->
AccessRules = ets:select(access,
AccessRules = mnesia:dirty_select(access,
[{{access, {'$1', Host}, '$2'}, [],
[{{access, '$1', '$2'}}]}]),
case lists:keysearch(<<"addnew">>, 1, Query) of
@ -1337,7 +1344,7 @@ parse_access_rule(Text) ->
list_vhosts(Lang, JID) ->
Hosts = (?MYHOSTS),
HostsAllowed = lists:filter(fun (Host) ->
is_acl_match(Host,
acl:any_rules_allowed(Host,
[configure, webadmin_view],
JID)
end,
@ -1549,7 +1556,7 @@ su_to_list({Server, User}) ->
%%%% get_stats
get_stats(global, Lang) ->
OnlineUsers = mnesia:table_info(session, size),
OnlineUsers = ejabberd_sm:connected_users_number(),
RegisteredUsers = lists:foldl(fun (Host, Total) ->
ejabberd_auth:get_vh_registered_users_number(Host)
+ Total
@ -2175,7 +2182,7 @@ get_node(global, Node, [<<"stats">>], _Query, Lang) ->
CPUTime = ejabberd_cluster:call(Node, erlang, statistics, [runtime]),
CPUTimeS = list_to_binary(io_lib:format("~.3f",
[element(1, CPUTime) / 1000])),
OnlineUsers = mnesia:table_info(session, size),
OnlineUsers = ejabberd_sm:connected_users_number(),
TransactionsCommitted = ejabberd_cluster:call(Node, mnesia,
system_info, [transaction_commits]),
TransactionsAborted = ejabberd_cluster:call(Node, mnesia,
@ -2970,7 +2977,8 @@ make_menu_item(item, 3, URI, Name, Lang) ->
%%%==================================
opt_type(access) -> fun (V) -> V end;
opt_type(_) -> [access].
opt_type(access) -> fun acl:access_rules_validator/1;
opt_type(access_readonly) -> fun acl:access_rules_validator/1;
opt_type(_) -> [access, access_readonly].
%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=:

View File

@ -47,7 +47,8 @@
-record(state,
{access_commands = [] :: list(),
auth = noauth :: noauth | {binary(), binary(), binary()},
get_auth = true :: boolean()}).
get_auth = true :: boolean(),
ip :: inet:ip_address()}).
%% Test:
@ -195,7 +196,7 @@ socket_type() -> raw.
%% -----------------------------
%% HTTP interface
%% -----------------------------
process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
process(_, #request{method = 'POST', data = Data, opts = Opts, ip = {IP, _}}) ->
AccessCommandsOpts = gen_mod:get_opt(access_commands, Opts,
fun(L) when is_list(L) -> L end,
undefined),
@ -206,7 +207,7 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
lists:flatmap(
fun({Ac, AcOpts}) ->
Commands = gen_mod:get_opt(
commands, AcOpts,
commands, lists:flatten(AcOpts),
fun(A) when is_atom(A) ->
A;
(L) when is_list(L) ->
@ -219,15 +220,15 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
options, AcOpts,
fun(L) when is_list(L) -> L end,
[]),
[{Ac, Commands, CommOpts}];
[{<<"ejabberd_xmlrpc compatibility shim">>, {[?MODULE], [{access, Ac}], Commands}}];
(Wrong) ->
?WARNING_MSG("wrong options format for ~p: ~p",
[?MODULE, Wrong]),
[]
end, AccessCommandsOpts)
end, lists:flatten(AccessCommandsOpts))
end,
GetAuth = true,
State = #state{access_commands = AccessCommands, get_auth = GetAuth},
State = #state{access_commands = AccessCommands, get_auth = GetAuth, ip = IP},
case fxml_stream:parse_element(Data) of
{error, _} ->
{400, [],
@ -258,21 +259,35 @@ process(_, _) ->
%% Access verification
%% -----------------------------
get_auth(AuthList) ->
Admin =
case lists:keysearch(admin, 1, AuthList) of
{value, {admin, true}} -> true;
_ -> false
end,
extract_auth(AuthList) ->
?DEBUG("AUTHLIST ~p", [AuthList]),
try get_attrs([user, server, token], AuthList) of
[U, S, T] -> {U, S, {oauth, T}, Admin}
[U0, S0, T] ->
U = jid:nodeprep(U0),
S = jid:nameprep(S0),
case ejabberd_oauth:check_token(T) of
{ok, {U, S}, Scope} ->
#{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
{false, Reason} ->
{error, Reason};
_ ->
{error, not_found}
end
catch
exit:{attribute_not_found, _Attr, _} ->
try get_attrs([user, server, password], AuthList) of
[U, S, P] -> {U, S, P, Admin}
[U0, S0, P] ->
U = jid:nodeprep(U0),
S = jid:nameprep(S0),
case ejabberd_auth:check_password(U, <<"">>, S, P) of
true ->
#{usr => {U, S, <<"">>}, caller_server => S};
false ->
{error, invalid_auth}
end
catch
exit:{attribute_not_found, Attr, _} ->
throw({error, missing_auth_arguments, Attr})
exit:{attribute_not_found, _Attr, _} ->
#{}
end
end.
@ -300,12 +315,28 @@ get_auth(AuthList) ->
%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "79C1574A43BC995F2B145A299EF97277"}]}, 152]}).
%% {ok,{response,[152]}}
handler(#state{get_auth = true, auth = noauth} = State,
handler(#state{get_auth = true, auth = noauth, ip = IP} = State,
{call, Method,
[{struct, AuthList} | Arguments] = AllArgs}) ->
try get_auth(AuthList) of
try extract_auth(AuthList) of
{error, invalid_auth} ->
build_fault_response(-118,
"Invalid authentication data",
[]);
{error, not_found} ->
build_fault_response(-118,
"Invalid oauth token",
[]);
{error, expired} ->
build_fault_response(-118,
"Invalid oauth token",
[]);
{error, Value} ->
build_fault_response(-118,
"Invalid authentication data: ~p",
[Value]);
Auth ->
handler(State#state{get_auth = false, auth = Auth},
handler(State#state{get_auth = false, auth = Auth#{ip => IP, caller_module => ?MODULE}},
{call, Method, Arguments})
catch
{error, missing_auth_arguments, _Attr} ->
@ -393,9 +424,14 @@ build_fault_response(Code, ParseString, ParseArgs) ->
do_command(AccessCommands, Auth, Command, AttrL, ArgsF,
ResultF) ->
ArgsFormatted = format_args(AttrL, ArgsF),
Auth2 = case AccessCommands of
V when is_list(V) ->
Auth#{extra_permissions => AccessCommands};
_ ->
Auth
end,
Result =
ejabberd_commands:execute_command(AccessCommands, Auth,
Command, ArgsFormatted),
ejabberd_commands:execute_command2(Command, ArgsFormatted, Auth2),
ResultFormatted = format_result(Result, ResultF),
{command_result, ResultFormatted}.
@ -489,6 +525,8 @@ process_unicode_codepoints(Str) ->
format_result({error, Error}, _) ->
throw({error, Error});
format_result({error, _Type, _Code, Error}, _) ->
throw({error, Error});
format_result(String, string) -> lists:flatten(String);
format_result(Atom, {Name, atom}) ->
{struct,

View File

@ -484,17 +484,28 @@ compile_deps(_Module, _Spec, DestDir) ->
filelib:ensure_dir(filename:join(Ebin, ".")),
Result = lists:foldl(fun(Dep, Acc) ->
Inc = filename:join(Dep, "include"),
Lib = filename:join(Dep, "lib"),
Src = filename:join(Dep, "src"),
Options = [{outdir, Ebin}, {i, Inc}],
[file:copy(App, Ebin) || App <- filelib:wildcard(Src++"/*.app")],
Acc++[case compile:file(File, Options) of
%% Compile erlang files
Acc1 = Acc ++ [case compile:file(File, Options) of
{ok, _} -> ok;
{ok, _, _} -> ok;
{ok, _, _, _} -> ok;
error -> {error, {compilation_failed, File}};
Error -> Error
end
|| File <- filelib:wildcard(Src++"/*.erl")]
|| File <- filelib:wildcard(Src++"/*.erl")],
%% Compile elixir files
Acc1 ++ [case compile_elixir_file(Ebin, File) of
{ok, _} -> ok;
{error, File} -> {error, {compilation_failed, File}}
end
|| File <- filelib:wildcard(Lib ++ "/*.ex")]
end, [], filelib:wildcard("deps/*")),
case lists:dropwhile(
fun(ok) -> true;
@ -515,6 +526,8 @@ compile(_Module, _Spec, DestDir) ->
verbose, report_errors, report_warnings]
++ ExtLib,
[file:copy(App, Ebin) || App <- filelib:wildcard("src/*.app")],
%% Compile erlang files
Result = [case compile:file(File, Options) of
{ok, _} -> ok;
{ok, _, _} -> ok;
@ -523,14 +536,32 @@ compile(_Module, _Spec, DestDir) ->
Error -> Error
end
|| File <- filelib:wildcard("src/*.erl")],
%% Compile elixir files
Result1 = Result ++ [case compile_elixir_file(Ebin, File) of
{ok, _} -> ok;
{error, File} -> {error, {compilation_failed, File}}
end
|| File <- filelib:wildcard("lib/*.ex")],
case lists:dropwhile(
fun(ok) -> true;
(_) -> false
end, Result) of
end, Result1) of
[] -> ok;
[Error|_] -> Error
end.
compile_elixir_file(Dest, File) when is_list(Dest) and is_list(File) ->
compile_elixir_file(list_to_binary(Dest), list_to_binary(File));
compile_elixir_file(Dest, File) ->
try 'Elixir.Kernel.ParallelCompiler':files_to_path([File], Dest, []) of
[Module] -> {ok, Module}
catch
_ -> {error, File}
end.
install(Module, Spec, DestDir) ->
Errors = lists:dropwhile(fun({_, {ok, _}}) -> true;
(_) -> false

View File

@ -102,8 +102,7 @@ call_port(Server, Msg) ->
receive {eauth, Result} -> Result end.
random_instance(MaxNum) ->
random:seed(p1_time_compat:timestamp()),
random:uniform(MaxNum) - 1.
randoms:uniform(MaxNum) - 1.
get_instances(Server) ->
ejabberd_config:get_option(

View File

@ -48,7 +48,7 @@
opts = [] :: opts() | '_' | '$2'}).
-type opts() :: [{atom(), any()}].
-type db_type() :: sql | mnesia | riak | ldap.
-type db_type() :: sql | mnesia | riak.
-callback start(binary(), opts()) -> any().
-callback stop(binary()) -> any().
@ -147,7 +147,7 @@ start_module(Host, Module) ->
-spec start_module(binary(), atom(), opts()) -> any().
start_module(Host, Module, Opts0) ->
Opts = validate_opts(Host, Module, Opts0),
Opts = validate_opts(Module, Opts0),
ets:insert(ejabberd_modules,
#ejabberd_module{module_host = {Module, Host},
opts = Opts}),
@ -308,10 +308,47 @@ get_opt_host(Host, Opts, Default) ->
Val = get_opt(host, Opts, fun iolist_to_binary/1, Default),
ejabberd_regexp:greplace(Val, <<"@HOST@">>, Host).
validate_opts(Host, Module, Opts) ->
get_module_mod_opt_type_fun(Module) ->
DBSubMods = ejabberd_config:v_dbs_mods(Module),
fun(Opt) ->
Res = lists:foldl(fun(Mod, {Funs, ArgsList, _} = Acc) ->
case catch Mod:mod_opt_type(Opt) of
Fun when is_function(Fun) ->
{[Fun | Funs], ArgsList, true};
L when is_list(L) ->
{Funs, L ++ ArgsList, true};
_ ->
Acc
end
end, {[], [], false}, [Module | DBSubMods]),
case Res of
{[], [], false} ->
throw({'EXIT', {undef, mod_opt_type}});
{[], Args, _} -> Args;
{Funs, _, _} ->
fun(Val) ->
lists:any(fun(F) ->
try F(Val) of
_ ->
true
catch {replace_with, _NewVal} = E ->
throw(E);
{invalid_syntax, _Error} = E2 ->
throw(E2);
_:_ ->
false
end
end, Funs)
end
end
end.
validate_opts(Module, Opts) ->
ModOptFun = get_module_mod_opt_type_fun(Module),
lists:filtermap(
fun({Opt, Val}) ->
case catch validate_opt(Host, Module, Opt, Opts) of
case catch ModOptFun(Opt) of
VFun when is_function(VFun) ->
try VFun(Val) of
_ ->
@ -346,22 +383,6 @@ validate_opts(Host, Module, Opts) ->
false
end, Opts).
validate_opt(Host, Module, Opt, Opts) ->
case Module:mod_opt_type(Opt) of
VFun1 when is_function(VFun1) ->
VFun1;
L1 when is_list(L1) ->
DBModule = db_mod(Host, Opts, Module),
try DBModule:mod_opt_type(Opt) of
VFun2 when is_function(VFun2) ->
VFun2;
L2 when is_list(L2) ->
lists:usort(L1 ++ L2)
catch _:undef ->
L1
end
end.
-spec db_type(binary() | global, module()) -> db_type();
(opts(), module()) -> db_type().
@ -378,7 +399,7 @@ db_type(Host, Module) when is_atom(Module) ->
undefined
end.
-spec db_type(global | binary(), opts(), module()) -> db_type().
-spec db_type(binary(), opts(), module()) -> db_type().
db_type(Host, Opts, Module) ->
case catch Module:mod_opt_type(db_type) of

358
src/http_p1.erl Normal file
View File

@ -0,0 +1,358 @@
%%%----------------------------------------------------------------------
%%% File : http_p1.erl
%%% Author : Emilio Bustos <ebustos@process-one.net>
%%% Purpose : Provide a common API for inets / lhttpc / ibrowse
%%% Created : 29 Jul 2010 by Emilio Bustos <ebustos@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
-module(http_p1).
-author('ebustos@process-one.net').
-export([start/0, stop/0, get/1, get/2, post/2, post/3,
request/3, request/4, request/5,
get_pool_size/0, set_pool_size/1]).
-include("logger.hrl").
-define(USE_INETS, 1).
% -define(USE_LHTTPC, 1).
% -define(USE_IBROWSE, 1).
% inets used as default if none specified
-ifdef(USE_IBROWSE).
start() ->
ejabberd:start_app(ibrowse).
stop() ->
application:stop(ibrowse).
request(Method, URL, Hdrs, Body, Opts) ->
TimeOut = proplists:get_value(timeout, Opts, infinity),
Options = [{inactivity_timeout, TimeOut}
| proplists:delete(timeout, Opts)],
case ibrowse:send_req(URL, Hdrs, Method, Body, Options)
of
{ok, Status, Headers, Response} ->
{ok, jlib:binary_to_integer(Status), Headers,
Response};
{error, Reason} -> {error, Reason}
end.
get_pool_size() ->
application:get_env(ibrowse, default_max_sessions, 10).
set_pool_size(Size) ->
application:set_env(ibrowse, default_max_sessions, Size).
-else.
-ifdef(USE_LHTTPC).
start() ->
ejabberd:start_app(lhttpc).
stop() ->
application:stop(lhttpc).
request(Method, URL, Hdrs, Body, Opts) ->
{[TO, SO], Rest} = proplists:split(Opts, [timeout, socket_options]),
TimeOut = proplists:get_value(timeout, TO, infinity),
SockOpt = proplists:get_value(socket_options, SO, []),
Options = [{connect_options, SockOpt} | Rest],
Result = lhttpc:request(URL, Method, Hdrs, Body, TimeOut, Options),
?DEBUG("HTTP request -> response:~n"
"** Method = ~p~n"
"** URI = ~s~n"
"** Body = ~s~n"
"** Hdrs = ~p~n"
"** Timeout = ~p~n"
"** Options = ~p~n"
"** Response = ~p",
[Method, URL, Body, Hdrs, TimeOut, Options, Result]),
case Result of
{ok, {{Status, _Reason}, Headers, Response}} ->
{ok, Status, Headers, (Response)};
{error, Reason} -> {error, Reason}
end.
get_pool_size() ->
Opts = proplists:get_value(lhttpc_manager, lhttpc_manager:list_pools()),
proplists:get_value(max_pool_size,Opts).
set_pool_size(Size) ->
lhttpc_manager:set_max_pool_size(lhttpc_manager, Size).
-else.
start() ->
ejabberd:start_app(inets).
stop() ->
application:stop(inets).
to_list(Str) when is_binary(Str) ->
binary_to_list(Str);
to_list(Str) ->
Str.
request(Method, URLRaw, HdrsRaw, Body, Opts) ->
Hdrs = lists:map(fun({N, V}) ->
{to_list(N), to_list(V)}
end, HdrsRaw),
URL = to_list(URLRaw),
Request = case Method of
get -> {URL, Hdrs};
head -> {URL, Hdrs};
delete -> {URL, Hdrs};
_ -> % post, etc.
{URL, Hdrs,
to_list(proplists:get_value(<<"content-type">>, HdrsRaw, [])),
Body}
end,
Options = case proplists:get_value(timeout, Opts,
infinity)
of
infinity -> proplists:delete(timeout, Opts);
_ -> Opts
end,
case httpc:request(Method, Request, Options, []) of
{ok, {{_, Status, _}, Headers, Response}} ->
{ok, Status, Headers, Response};
{error, Reason} -> {error, Reason}
end.
get_pool_size() ->
{ok, Size} = httpc:get_option(max_sessions),
Size.
set_pool_size(Size) ->
httpc:set_option(max_sessions, Size).
-endif.
-endif.
-type({header,
{type, 63, tuple,
[{type, 63, union,
[{type, 63, string, []}, {type, 63, atom, []}]},
{type, 63, string, []}]},
[]}).
-type({headers,
{type, 64, list, [{type, 64, header, []}]}, []}).
-type({option,
{type, 67, union,
[{type, 67, tuple,
[{atom, 67, connect_timeout}, {type, 67, timeout, []}]},
{type, 68, tuple,
[{atom, 68, timeout}, {type, 68, timeout, []}]},
{type, 70, tuple,
[{atom, 70, send_retry},
{type, 70, non_neg_integer, []}]},
{type, 71, tuple,
[{atom, 71, partial_upload},
{type, 71, union,
[{type, 71, non_neg_integer, []},
{atom, 71, infinity}]}]},
{type, 72, tuple,
[{atom, 72, partial_download}, {type, 72, pid, []},
{type, 72, union,
[{type, 72, non_neg_integer, []},
{atom, 72, infinity}]}]}]},
[]}).
-type({options,
{type, 74, list, [{type, 74, option, []}]}, []}).
-type({result,
{type, 76, union,
[{type, 76, tuple,
[{atom, 76, ok},
{type, 76, tuple,
[{type, 76, tuple,
[{type, 76, pos_integer, []}, {type, 76, string, []}]},
{type, 76, headers, []}, {type, 76, string, []}]}]},
{type, 77, tuple,
[{atom, 77, error}, {type, 77, atom, []}]}]},
[]}).
%% @spec (URL) -> Result
%% URL = string()
%% Result = {ok, StatusCode, Hdrs, ResponseBody}
%% | {error, Reason}
%% StatusCode = integer()
%% ResponseBody = string()
%% Reason = connection_closed | connect_timeout | timeout
%% @doc Sends a GET request.
%% Would be the same as calling `request(get, URL, [])',
%% that is {@link request/3} with an empty header list.
%% @end
%% @see request/3
-spec get(string()) -> result().
get(URL) -> request(get, URL, []).
%% @spec (URL, Hdrs) -> Result
%% URL = string()
%% Hdrs = [{Header, Value}]
%% Header = string()
%% Value = string()
%% Result = {ok, StatusCode, Hdrs, ResponseBody}
%% | {error, Reason}
%% StatusCode = integer()
%% ResponseBody = string()
%% Reason = connection_closed | connect_timeout | timeout
%% @doc Sends a GET request.
%% Would be the same as calling `request(get, URL, Hdrs)'.
%% @end
%% @see request/3
-spec get(string(), headers()) -> result().
get(URL, Hdrs) -> request(get, URL, Hdrs).
%% @spec (URL, RequestBody) -> Result
%% URL = string()
%% RequestBody = string()
%% Result = {ok, StatusCode, Hdrs, ResponseBody}
%% | {error, Reason}
%% StatusCode = integer()
%% ResponseBody = string()
%% Reason = connection_closed | connect_timeout | timeout
%% @doc Sends a POST request with form data.
%% Would be the same as calling
%% `request(post, URL, [{"content-type", "x-www-form-urlencoded"}], Body)'.
%% @end
%% @see request/4
-spec post(string(), string()) -> result().
post(URL, Body) ->
request(post, URL,
[{<<"content-type">>, <<"x-www-form-urlencoded">>}],
Body).
%% @spec (URL, Hdrs, RequestBody) -> Result
%% URL = string()
%% Hdrs = [{Header, Value}]
%% Header = string()
%% Value = string()
%% RequestBody = string()
%% Result = {ok, StatusCode, Hdrs, ResponseBody}
%% | {error, Reason}
%% StatusCode = integer()
%% ResponseBody = string()
%% Reason = connection_closed | connect_timeout | timeout
%% @doc Sends a POST request.
%% Would be the same as calling
%% `request(post, URL, Hdrs, Body)'.
%% @end
%% @see request/4
-spec post(string(), headers(), string()) -> result().
post(URL, Hdrs, Body) ->
NewHdrs = case [X
|| {X, _} <- Hdrs,
str:to_lower(X) == <<"content-type">>]
of
[] ->
[{<<"content-type">>, <<"x-www-form-urlencoded">>}
| Hdrs];
_ -> Hdrs
end,
request(post, URL, NewHdrs, Body).
%% @spec (Method, URL, Hdrs) -> Result
%% Method = atom()
%% URL = string()
%% Hdrs = [{Header, Value}]
%% Header = string()
%% Value = string()
%% Result = {ok, StatusCode, Hdrs, ResponseBody}
%% | {error, Reason}
%% StatusCode = integer()
%% ResponseBody = string()
%% Reason = connection_closed | connect_timeout | timeout
%% @doc Sends a request without a body.
%% Would be the same as calling `request(Method, URL, Hdrs, [], [])',
%% that is {@link request/5} with an empty body.
%% @end
%% @see request/5
-spec request(atom(), string(), headers()) -> result().
request(Method, URL, Hdrs) ->
request(Method, URL, Hdrs, [], []).
%% @spec (Method, URL, Hdrs, RequestBody) -> Result
%% Method = atom()
%% URL = string()
%% Hdrs = [{Header, Value}]
%% Header = string()
%% Value = string()
%% RequestBody = string()
%% Result = {ok, StatusCode, Hdrs, ResponseBody}
%% | {error, Reason}
%% StatusCode = integer()
%% ResponseBody = string()
%% Reason = connection_closed | connect_timeout | timeout
%% @doc Sends a request with a body.
%% Would be the same as calling
%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5}
%% with no options.
%% @end
%% @see request/5
-spec request(atom(), string(), headers(), string()) -> result().
request(Method, URL, Hdrs, Body) ->
request(Method, URL, Hdrs, Body, []).
%% @spec (Method, URL, Hdrs, RequestBody, Options) -> Result
%% Method = atom()
%% URL = string()
%% Hdrs = [{Header, Value}]
%% Header = string()
%% Value = string()
%% RequestBody = string()
%% Options = [Option]
%% Option = {timeout, Milliseconds | infinity} |
%% {connect_timeout, Milliseconds | infinity} |
%% {socket_options, [term()]} |
%% Milliseconds = integer()
%% Result = {ok, StatusCode, Hdrs, ResponseBody}
%% | {error, Reason}
%% StatusCode = integer()
%% ResponseBody = string()
%% Reason = connection_closed | connect_timeout | timeout
%% @doc Sends a request with a body.
%% Would be the same as calling
%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5}
%% with no options.
%% @end
%% @see request/5
-spec request(atom(), string(), headers(), string(), options()) -> result().
% ibrowse {response_format, response_format()} |
% Options - [option()]
% Option - {sync, boolean()} | {stream, StreamTo} | {body_format, body_format()} | {full_result,
% boolean()} | {headers_as_is, boolean()}
%body_format() = string() | binary()
% The body_format option is only valid for the synchronous request and the default is string.
% When making an asynchronous request the body will always be received as a binary.
% lhttpc: always binary

View File

@ -52,11 +52,35 @@
-spec start() -> ok.
start() ->
{ok, Owner} = ets_owner(),
SplitPattern = binary:compile_pattern([<<"@">>, <<"/">>]),
catch ets:new(jlib, [named_table, protected, set, {keypos, 1}]),
%% Table is public to allow ETS insert to fix / update the table even if table already exist
%% with another owner.
catch ets:new(jlib, [named_table, public, set, {keypos, 1}, {heir, Owner, undefined}]),
ets:insert(jlib, {string_to_jid_pattern, SplitPattern}),
ok.
ets_owner() ->
case whereis(jlib_ets) of
undefined ->
Pid = spawn(fun() -> ets_keepalive() end),
case catch register(jlib_ets, Pid) of
true ->
{ok, Pid};
Error -> Error
end;
Pid ->
{ok,Pid}
end.
%% Process used to keep jlib ETS table alive in case the original owner dies.
%% The table need to be public, otherwise subsequent inserts would fail.
ets_keepalive() ->
receive
_ ->
ets_keepalive()
end.
-spec make(binary(), binary(), binary()) -> jid() | error.
make(User, Server, Resource) ->

View File

@ -373,15 +373,20 @@ iq_type_to_string(error) -> <<"error">>.
-spec iq_to_xml(IQ :: iq()) -> xmlel().
iq_to_xml(#iq{id = ID, type = Type, sub_el = SubEl}) ->
Children =
if
is_list(SubEl) -> SubEl;
true -> [SubEl]
end,
if ID /= <<"">> ->
#xmlel{name = <<"iq">>,
attrs =
[{<<"id">>, ID}, {<<"type">>, iq_type_to_string(Type)}],
children = SubEl};
children = Children};
true ->
#xmlel{name = <<"iq">>,
attrs = [{<<"type">>, iq_type_to_string(Type)}],
children = SubEl}
children = Children}
end.
-spec parse_xdata_submit(El :: xmlel()) ->
@ -579,33 +584,8 @@ add_delay_info(El, From, Time) ->
binary()) -> xmlel().
add_delay_info(El, From, Time, Desc) ->
case fxml:get_subtag_with_xmlns(El, <<"delay">>, ?NS_DELAY) of
false ->
%% Add new tag
DelayTag = create_delay_tag(Time, From, Desc),
fxml:append_subtags(El, [DelayTag]);
DelayTag ->
%% Update existing tag
NewDelayTag =
case {fxml:get_tag_cdata(DelayTag), Desc} of
{<<"">>, <<"">>} ->
DelayTag;
{OldDesc, <<"">>} ->
DelayTag#xmlel{children = [{xmlcdata, OldDesc}]};
{<<"">>, NewDesc} ->
DelayTag#xmlel{children = [{xmlcdata, NewDesc}]};
{OldDesc, NewDesc} ->
case binary:match(OldDesc, NewDesc) of
nomatch ->
FinalDesc = <<OldDesc/binary, ", ", NewDesc/binary>>,
DelayTag#xmlel{children = [{xmlcdata, FinalDesc}]};
_ ->
DelayTag#xmlel{children = [{xmlcdata, OldDesc}]}
end
end,
NewEl = fxml:remove_subtags(El, <<"delay">>, {<<"xmlns">>, ?NS_DELAY}),
fxml:append_subtags(NewEl, [NewDelayTag])
end.
fxml:append_subtags(El, [DelayTag]).
-spec create_delay_tag(erlang:timestamp(), jid() | ljid() | binary(), binary())
-> xmlel() | error.

View File

@ -378,6 +378,7 @@ get_commands_spec() ->
#ejabberd_commands{name = add_rosteritem, tags = [roster],
desc = "Add an item to a user's roster (supports ODBC)",
longdesc = "Group can be several groups separated by ; for example: \"g1;g2;g3\"",
module = ?MODULE, function = add_rosteritem,
args = [{localuser, binary}, {localserver, binary},
{user, binary}, {server, binary},
@ -536,7 +537,7 @@ get_commands_spec() ->
policy = user,
module = mod_offline, function = count_offline_messages,
args = [],
result = {res, integer}},
result = {value, integer}},
#ejabberd_commands{name = send_message, tags = [stanza],
desc = "Send a message to a local or remote bare of full JID",
module = ?MODULE, function = send_message,
@ -864,12 +865,15 @@ connected_users_vhost(Host) ->
%% Code copied from ejabberd_sm.erl and customized
dirty_get_sessions_list2() ->
mnesia:dirty_select(
Ss = mnesia:dirty_select(
session,
[{#session{usr = '$1', sid = {'$2', '$3'}, priority = '$4', info = '$5',
[{#session{usr = '$1', sid = '$2', priority = '$3', info = '$4',
_ = '_'},
[{is_pid, '$3'}],
[['$1', {{'$2', '$3'}}, '$4', '$5']]}]).
[],
[['$1', '$2', '$3', '$4']]}]),
lists:filter(fun([_USR, _SID, _Priority, Info]) ->
not proplists:get_bool(offline, Info)
end, Ss).
%% Make string more print-friendly
stringize(String) ->
@ -906,8 +910,8 @@ user_sessions_info(User, Host) ->
{'EXIT', _Reason} ->
[];
Ss ->
lists:filter(fun(#session{sid = {_, Pid}}) ->
is_pid(Pid)
lists:filter(fun(#session{info = Info}) ->
not proplists:get_bool(offline, Info)
end, Ss)
end,
lists:map(
@ -1140,8 +1144,8 @@ subscribe_roster({Name, Server, Group, Nick}, [{Name, Server, _, _} | Roster]) -
subscribe_roster({Name, Server, Group, Nick}, Roster);
%% Subscribe Name2 to Name1
subscribe_roster({Name1, Server1, Group1, Nick1}, [{Name2, Server2, Group2, Nick2} | Roster]) ->
subscribe(Name1, Server1, list_to_binary(Name2), list_to_binary(Server2),
list_to_binary(Nick2), list_to_binary(Group2), <<"both">>, []),
subscribe(Name1, Server1, iolist_to_binary(Name2), iolist_to_binary(Server2),
iolist_to_binary(Nick2), iolist_to_binary(Group2), <<"both">>, []),
subscribe_roster({Name1, Server1, Group1, Nick1}, Roster).
push_alltoall(S, G) ->
@ -1173,10 +1177,11 @@ push_roster_item(LU, LS, R, U, S, Action) ->
ejabberd_router:route(jid:remove_resource(LJID), LJID, ResIQ).
build_roster_item(U, S, {add, Nick, Subs, Group}) ->
Groups = binary:split(Group,<<";">>, [global]),
#roster_item{jid = jid:make(U, S),
name = Nick,
subscription = jlib:binary_to_atom(Subs),
groups = [Group]};
groups = Groups};
build_roster_item(U, S, remove) ->
#roster_item{jid = jid:make(U, S), subscription = remove}.
@ -1260,11 +1265,11 @@ srg_create(Group, Host, Name, Description, Display) ->
Opts = [{name, Name},
{displayed_groups, DisplayList},
{description, Description}],
{atomic, ok} = mod_shared_roster:create_group(Host, Group, Opts),
{atomic, _} = mod_shared_roster:create_group(Host, Group, Opts),
ok.
srg_delete(Group, Host) ->
{atomic, ok} = mod_shared_roster:delete_group(Host, Group),
{atomic, _} = mod_shared_roster:delete_group(Host, Group),
ok.
srg_list(Host) ->
@ -1287,11 +1292,11 @@ srg_get_members(Group, Host) ->
|| {MUser, MServer} <- Members].
srg_user_add(User, Host, Group, GroupHost) ->
{atomic, ok} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group),
{atomic, _} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group),
ok.
srg_user_del(User, Host, Group, GroupHost) ->
{atomic, ok} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group),
{atomic, _} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group),
ok.
@ -1302,44 +1307,9 @@ srg_user_del(User, Host, Group, GroupHost) ->
%% @doc Send a message to a Jabber account.
%% @spec (Type::binary(), From::binary(), To::binary(), Subject::binary(), Body::binary()) -> ok
send_message(Type, From, To, Subject, Body) ->
FromJID = jid:from_string(From),
ToJID = jid:from_string(To),
Packet = build_packet(Type, Subject, Body),
send_packet_all_resources(From, To, Packet).
%% @doc Send a packet to a Jabber account.
%% If a resource was specified in the JID,
%% the packet is sent only to that specific resource.
%% If no resource was specified in the JID,
%% and the user is remote or local but offline,
%% the packet is sent to the bare JID.
%% If the user is local and is online in several resources,
%% the packet is sent to all its resources.
send_packet_all_resources(FromJIDString, ToJIDString, Packet) ->
FromJID = jid:from_string(FromJIDString),
ToJID = jid:from_string(ToJIDString),
ToUser = ToJID#jid.user,
ToServer = ToJID#jid.server,
case ToJID#jid.resource of
<<>> ->
send_packet_all_resources(FromJID, ToUser, ToServer, Packet);
Res ->
send_packet_all_resources(FromJID, ToUser, ToServer, Res, Packet)
end.
send_packet_all_resources(FromJID, ToUser, ToServer, Packet) ->
case ejabberd_sm:get_user_resources(ToUser, ToServer) of
[] ->
send_packet_all_resources(FromJID, ToUser, ToServer, <<>>, Packet);
ToResources ->
lists:foreach(
fun(ToResource) ->
send_packet_all_resources(FromJID, ToUser, ToServer,
ToResource, Packet)
end,
ToResources)
end.
send_packet_all_resources(FromJID, ToU, ToS, ToR, Packet) ->
ToJID = jid:make(ToU, ToS, ToR),
ejabberd_router:route(FromJID, ToJID, Packet).
build_packet(Type, Subject, Body) ->

View File

@ -609,8 +609,8 @@ announce_all(From, To, Packet) ->
Local = jid:make(To#jid.server),
lists:foreach(
fun({User, Server}) ->
Dest = jid:make(User, Server),
ejabberd_router:route(Local, Dest, Packet)
Dest = jid:make(User, Server, <<>>),
ejabberd_router:route(Local, Dest, add_store_hint(Packet))
end, ejabberd_auth:get_vh_registered_users(Host))
end.
@ -626,8 +626,8 @@ announce_all_hosts_all(From, To, Packet) ->
Local = jid:make(To#jid.server),
lists:foreach(
fun({User, Server}) ->
Dest = jid:make(User, Server),
ejabberd_router:route(Local, Dest, Packet)
Dest = jid:make(User, Server, <<>>),
ejabberd_router:route(Local, Dest, add_store_hint(Packet))
end, ejabberd_auth:dirty_get_registered_users())
end.
@ -813,7 +813,7 @@ send_announcement_to_all(Host, SubjectS, BodyS) ->
lists:foreach(
fun({U, S, R}) ->
Dest = jid:make(U, S, R),
ejabberd_router:route(Local, Dest, Packet)
ejabberd_router:route(Local, Dest, add_store_hint(Packet))
end, Sessions).
-spec get_access(global | binary()) -> atom().
@ -823,6 +823,10 @@ get_access(Host) ->
fun(A) -> A end,
none).
-spec add_store_hint(stanza()) -> stanza().
add_store_hint(El) ->
xmpp:set_subtag(El, #hint{type = store}).
%%-------------------------------------------------------------------------
export(LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),

View File

@ -123,6 +123,7 @@ user_receive_packet(Packet, _C2SState, JID, _From, To) ->
stanza() | {stop, stanza()}.
check_and_forward(JID, To, Packet, Direction)->
case is_chat_message(Packet) andalso
not is_muc_pm(To, Packet) andalso
xmpp:has_subtag(Packet, #carbons_private{}) == false andalso
xmpp:has_subtag(Packet, #hint{type = 'no-copy'}) == false of
true ->
@ -232,6 +233,11 @@ is_chat_message(#message{type = normal, body = Body}) ->
is_chat_message(_) ->
false.
is_muc_pm(#jid{lresource = <<>>}, _Packet) ->
false;
is_muc_pm(_To, Packet) ->
xmpp:has_subtag(Packet, #muc_user{}).
-spec list(binary(), binary()) -> [{binary(), binary()}].
%% list {resource, cc_version} with carbons enabled for given user and host
list(User, Server) ->

View File

@ -34,8 +34,8 @@
-export([start/2, stop/1, mod_opt_type/1, depends/2]).
%% ejabberd_hooks callbacks.
-export([filter_presence/3, filter_chat_states/3, filter_pep/3, filter_other/3,
flush_queue/2, add_stream_feature/2]).
-export([filter_presence/4, filter_chat_states/4, filter_pep/4, filter_other/4,
flush_queue/3, add_stream_feature/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@ -151,26 +151,27 @@ depends(_Host, _Opts) ->
%% ejabberd_hooks callbacks.
%%--------------------------------------------------------------------
-spec filter_presence({ejabberd_c2s:state(), [stanza()]}, binary(), stanza())
-spec filter_presence({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza())
-> {ejabberd_c2s:state(), [stanza()]} |
{stop, {ejabberd_c2s:state(), [stanza()]}}.
filter_presence({C2SState, _OutStanzas} = Acc, Host,
filter_presence({C2SState, _OutStanzas} = Acc, Host, To,
#presence{type = Type} = Stanza) ->
if Type == available; Type == unavailable ->
?DEBUG("Got availability presence stanza", []),
?DEBUG("Got availability presence stanza for ~s",
[jid:to_string(To)]),
queue_add(presence, Stanza, Host, C2SState);
true ->
Acc
end;
filter_presence(Acc, _Host, _Stanza) -> Acc.
filter_presence(Acc, _Host, _To, _Stanza) -> Acc.
-spec filter_chat_states({ejabberd_c2s:state(), [stanza()]}, binary(), stanza())
-spec filter_chat_states({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza())
-> {ejabberd_c2s:state(), [stanza()]} |
{stop, {ejabberd_c2s:state(), [stanza()]}}.
filter_chat_states({C2SState, _OutStanzas} = Acc, Host,
#message{from = From, to = To} = Stanza) ->
filter_chat_states({C2SState, _OutStanzas} = Acc, Host, To,
#message{from = From} = Stanza) ->
case xmpp_util:is_standalone_chat_state(Stanza) of
true ->
case {From, To} of
@ -180,40 +181,41 @@ filter_chat_states({C2SState, _OutStanzas} = Acc, Host,
%% conversations across clients.
Acc;
_ ->
?DEBUG("Got standalone chat state notification", []),
?DEBUG("Got standalone chat state notification for ~s",
[jid:to_string(To)]),
queue_add(chatstate, Stanza, Host, C2SState)
end;
false ->
Acc
end;
filter_chat_states(Acc, _Host, _Stanza) -> Acc.
filter_chat_states(Acc, _Host, _To, _Stanza) -> Acc.
-spec filter_pep({ejabberd_c2s:state(), [stanza()]}, binary(), stanza())
-spec filter_pep({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza())
-> {ejabberd_c2s:state(), [stanza()]} |
{stop, {ejabberd_c2s:state(), [stanza()]}}.
filter_pep({C2SState, _OutStanzas} = Acc, Host, #message{} = Stanza) ->
filter_pep({C2SState, _OutStanzas} = Acc, Host, To, #message{} = Stanza) ->
case get_pep_node(Stanza) of
undefined ->
Acc;
Node ->
?DEBUG("Got PEP notification", []),
?DEBUG("Got PEP notification for ~s", [jid:to_string(To)]),
queue_add({pep, Node}, Stanza, Host, C2SState)
end;
filter_pep(Acc, _Host, _Stanza) -> Acc.
filter_pep(Acc, _Host, _To, _Stanza) -> Acc.
-spec filter_other({ejabberd_c2s:state(), [stanza()]}, binary(), stanza())
-> {stop, {ejabberd_c2s:state(), [stanza()]}}.
filter_other({C2SState, _OutStanzas}, Host, Stanza) ->
?DEBUG("Won't add stanza to CSI queue", []),
queue_take(Stanza, Host, C2SState).
-spec flush_queue({ejabberd_c2s:state(), [stanza()]}, binary())
-spec filter_other({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza())
-> {ejabberd_c2s:state(), [stanza()]}.
flush_queue({C2SState, _OutStanzas}, Host) ->
?DEBUG("Going to flush CSI queue", []),
filter_other({C2SState, _OutStanzas}, Host, To, Stanza) ->
?DEBUG("Won't add stanza for ~s to CSI queue", [jid:to_string(To)]),
queue_take(Stanza, Host, C2SState).
-spec flush_queue({ejabberd_c2s:state(), [stanza()]}, binary(), jid())
-> {ejabberd_c2s:state(), [stanza()]}.
flush_queue({C2SState, _OutStanzas}, Host, JID) ->
?DEBUG("Going to flush CSI queue of ~s", [jid:to_string(JID)]),
Queue = get_queue(C2SState),
NewState = set_queue([], C2SState),
{NewState, get_stanzas(Queue, Host)}.
@ -246,7 +248,7 @@ queue_add(Type, Stanza, Host, C2SState) ->
{stop, {NewState, []}}
end.
-spec queue_take(stanza(), binary(), term()) -> {stop, {term(), [stanza()]}}.
-spec queue_take(stanza(), binary(), term()) -> {term(), [stanza()]}.
queue_take(Stanza, Host, C2SState) ->
From = xmpp:get_from(Stanza),
@ -256,7 +258,7 @@ queue_take(Stanza, Host, C2SState) ->
U == LUser andalso S == LServer
end, get_queue(C2SState)),
NewState = set_queue(Rest, C2SState),
{stop, {NewState, get_stanzas(Selected, Host) ++ [Stanza]}}.
{NewState, get_stanzas(Selected, Host) ++ [Stanza]}.
-spec set_queue(csi_queue(), term()) -> term().

View File

@ -1084,7 +1084,7 @@ get_form(Host, [<<"config">>, <<"acls">>], Lang) ->
ACLs = str:tokens(
iolist_to_binary(
io_lib:format("~p.",
[ets:select(
[mnesia:dirty_select(
acl,
ets:fun2ms(
fun({acl, {Name, H}, Spec}) when H == Host ->
@ -1103,7 +1103,7 @@ get_form(Host, [<<"config">>, <<"access">>], Lang) ->
Accs = str:tokens(
iolist_to_binary(
io_lib:format("~p.",
[ets:select(
[mnesia:dirty_select(
access,
ets:fun2ms(
fun({access, {Name, H}, Acc}) when H == Host ->
@ -1568,21 +1568,29 @@ set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>),
Xmlelement = xmpp:serr_policy_violation(<<"has been kicked">>, Lang),
case JID#jid.lresource of
<<>> ->
SIDs = mnesia:dirty_select(session,
[{#session{sid = {'$1', '$2'},
usr = {LUser, LServer, '_'},
SIs = mnesia:dirty_select(session,
[{#session{usr = {LUser, LServer, '_'},
sid = '$1',
info = '$2',
_ = '_'},
[{is_pid, '$2'}],
[{{'$1', '$2'}}]}]),
[Pid ! {kick, kicked_by_admin, Xmlelement} || {_, Pid} <- SIDs];
R ->
[{_, Pid}] = mnesia:dirty_select(session,
[{#session{sid = {'$1', '$2'},
usr = {LUser, LServer, R},
_ = '_'},
[{is_pid, '$2'}],
[{{'$1', '$2'}}]}]),
[], [{{'$1', '$2'}}]}]),
Pids = [P || {{_, P}, Info} <- SIs,
not proplists:get_bool(offline, Info)],
lists:foreach(fun(Pid) ->
Pid ! {kick, kicked_by_admin, Xmlelement}
end, Pids);
R ->
[{{_, Pid}, Info}] = mnesia:dirty_select(
session,
[{#session{usr = {LUser, LServer, R},
sid = '$1',
info = '$2',
_ = '_'},
[], [{{'$1', '$2'}}]}]),
case proplists:get_bool(offline, Info) of
true -> ok;
false -> Pid ! {kick, kicked_by_admin, Xmlelement}
end
end,
{result, undefined};
set_form(From, Host,

325
src/mod_delegation.erl Normal file
View File

@ -0,0 +1,325 @@
%%%-------------------------------------------------------------------
%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
%%% @copyright (C) 2016, Evgeny Khramtsov
%%% @doc
%%%
%%% @end
%%% Created : 10 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
%%%-------------------------------------------------------------------
-module(mod_delegation).
-behaviour(gen_server).
-behaviour(gen_mod).
%% API
-export([start_link/2]).
-export([start/2, stop/1, mod_opt_type/1, depends/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([component_connected/1, component_disconnected/2,
ejabberd_local/1, ejabberd_sm/1,
disco_local_features/5, disco_sm_features/5,
disco_local_identity/5, disco_sm_identity/5]).
-include("ejabberd.hrl").
-include("logger.hrl").
-include("xmpp.hrl").
-type disco_acc() :: {error, stanza_error()} | {result, [binary()]} | empty.
-record(state, {server_host = <<"">> :: binary(),
delegations = dict:new() :: ?TDICT}).
%%%===================================================================
%%% API
%%%===================================================================
start_link(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?MODULE),
gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?MODULE),
PingSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
transient, 2000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, PingSpec).
stop(Host) ->
Proc = gen_mod:get_module_proc(Host, ?MODULE),
gen_server:call(Proc, stop),
supervisor:delete_child(ejabberd_sup, Proc).
mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
mod_opt_type(namespaces) -> validate_fun();
mod_opt_type(_) ->
[namespaces, iqdisc].
depends(_, _) ->
[].
-spec component_connected(binary()) -> ok.
component_connected(Host) ->
lists:foreach(
fun(ServerHost) ->
Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
gen_server:cast(Proc, {component_connected, Host})
end, ?MYHOSTS).
-spec component_disconnected(binary(), binary()) -> ok.
component_disconnected(Host, _Reason) ->
lists:foreach(
fun(ServerHost) ->
Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
gen_server:cast(Proc, {component_disconnected, Host})
end, ?MYHOSTS).
-spec ejabberd_local(iq()) -> iq().
ejabberd_local(IQ) ->
process_iq(IQ, ejabberd_local).
-spec ejabberd_sm(iq()) -> iq().
ejabberd_sm(IQ) ->
process_iq(IQ, ejabberd_sm).
-spec disco_local_features(disco_acc(), jid(), jid(), binary(), binary()) -> disco_acc().
disco_local_features(Acc, From, To, Node, Lang) ->
disco_features(Acc, From, To, Node, Lang, ejabberd_local).
-spec disco_sm_features(disco_acc(), jid(), jid(), binary(), binary()) -> disco_acc().
disco_sm_features(Acc, From, To, Node, Lang) ->
disco_features(Acc, From, To, Node, Lang, ejabberd_sm).
-spec disco_local_identity(disco_acc(), jid(), jid(), binary(), binary()) -> disco_acc().
disco_local_identity(Acc, From, To, Node, Lang) ->
disco_identity(Acc, From, To, Node, Lang, ejabberd_local).
-spec disco_sm_identity(disco_acc(), jid(), jid(), binary(), binary()) -> disco_acc().
disco_sm_identity(Acc, From, To, Node, Lang) ->
disco_identity(Acc, From, To, Node, Lang, ejabberd_sm).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([Host, _Opts]) ->
ejabberd_hooks:add(component_connected, ?MODULE,
component_connected, 50),
ejabberd_hooks:add(component_disconnected, ?MODULE,
component_disconnected, 50),
ejabberd_hooks:add(disco_local_features, Host, ?MODULE,
disco_local_features, 50),
ejabberd_hooks:add(disco_sm_features, Host, ?MODULE,
disco_sm_features, 50),
ejabberd_hooks:add(disco_local_identity, Host, ?MODULE,
disco_local_identity, 50),
ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE,
disco_sm_identity, 50),
{ok, #state{server_host = Host}}.
handle_call(get_delegations, _From, State) ->
{reply, {ok, State#state.delegations}, State};
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
handle_cast({component_connected, Host}, State) ->
ServerHost = State#state.server_host,
To = jid:make(Host),
NSAttrsAccessList = gen_mod:get_module_opt(
ServerHost, ?MODULE, namespaces,
validate_fun(), []),
lists:foreach(
fun({NS, _Attrs, Access}) ->
case acl:match_rule(ServerHost, Access, To) of
allow ->
send_disco_queries(ServerHost, Host, NS);
deny ->
ok
end
end, NSAttrsAccessList),
{noreply, State};
handle_cast({disco_info, Type, Host, NS, Info}, State) ->
From = jid:make(State#state.server_host),
To = jid:make(Host),
case dict:find({NS, Type}, State#state.delegations) of
error ->
Msg = #message{from = From, to = To,
sub_els = [#delegation{delegated = [#delegated{ns = NS}]}]},
Delegations = dict:store({NS, Type}, {Host, Info}, State#state.delegations),
gen_iq_handler:add_iq_handler(Type, State#state.server_host, NS,
?MODULE, Type, one_queue),
ejabberd_router:route(From, To, Msg),
?INFO_MSG("Namespace '~s' is delegated to external component '~s'",
[NS, Host]),
{noreply, State#state{delegations = Delegations}};
{ok, {AnotherHost, _}} ->
?WARNING_MSG("Failed to delegate namespace '~s' to "
"external component '~s' because it's already "
"delegated to '~s'",
[NS, Host, AnotherHost]),
{noreply, State}
end;
handle_cast({component_disconnected, Host}, State) ->
ServerHost = State#state.server_host,
Delegations =
dict:filter(
fun({NS, Type}, {H, _}) when H == Host ->
?INFO_MSG("Remove delegation of namespace '~s' "
"from external component '~s'",
[NS, Host]),
gen_iq_handler:remove_iq_handler(Type, ServerHost, NS),
false;
(_, _) ->
true
end, State#state.delegations),
{noreply, State#state{delegations = Delegations}};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, State) ->
%% Note: we don't remove component_* hooks because they are global
%% and might be registered within a module on another virtual host
ServerHost = State#state.server_host,
ejabberd_hooks:delete(disco_local_features, ServerHost, ?MODULE,
disco_local_features, 50),
ejabberd_hooks:delete(disco_sm_features, ServerHost, ?MODULE,
disco_sm_features, 50),
ejabberd_hooks:delete(disco_local_identity, ServerHost, ?MODULE,
disco_local_identity, 50),
ejabberd_hooks:delete(disco_sm_identity, ServerHost, ?MODULE,
disco_sm_identity, 50),
lists:foreach(
fun({NS, Type}) ->
gen_iq_handler:remove_iq_handler(Type, ServerHost, NS)
end, dict:fetch_keys(State#state.delegations)).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec get_delegations(binary()) -> ?TDICT.
get_delegations(Host) ->
Proc = gen_mod:get_module_proc(Host, ?MODULE),
try gen_server:call(Proc, get_delegations) of
{ok, Delegations} -> Delegations
catch exit:{noproc, _} ->
%% No module is loaded for this virtual host
dict:new()
end.
-spec process_iq(iq(), ejabberd_local | ejabberd_sm) -> ignore | iq().
process_iq(#iq{to = To, lang = Lang, sub_els = [SubEl]} = IQ, Type) ->
LServer = To#jid.lserver,
NS = xmpp:get_ns(SubEl),
Delegations = get_delegations(LServer),
case dict:find({NS, Type}, Delegations) of
{ok, {Host, _}} ->
Delegation = #delegation{forwarded = #forwarded{sub_els = [IQ]}},
NewFrom = jid:make(LServer),
NewTo = jid:make(Host),
ejabberd_local:route_iq(
NewFrom, NewTo,
#iq{type = set,
from = NewFrom,
to = NewTo,
sub_els = [Delegation]},
fun(Result) -> process_iq_result(IQ, Result) end),
ignore;
error ->
Txt = <<"Failed to map delegated namespace to external component">>,
xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
end.
-spec process_iq_result(iq(), iq()) -> ok.
process_iq_result(#iq{from = From, to = To, id = ID, lang = Lang} = IQ,
#iq{type = result} = ResIQ) ->
case xmpp:get_subtag(ResIQ, #delegation{}) of
#delegation{
forwarded = #forwarded{
sub_els = [#iq{from = To, to = From,
type = Type, id = ID} = Reply]}}
when Type == error; Type == result ->
ejabberd_router:route(From, To, Reply);
_ ->
?ERROR_MSG("got iq-result with invalid delegated "
"payload:~n~s", [xmpp:pp(ResIQ)]),
Txt = <<"External component failure">>,
Err = xmpp:err_internal_server_error(Txt, Lang),
ejabberd_router:route_error(To, From, IQ, Err)
end;
process_iq_result(#iq{from = From, to = To}, #iq{type = error} = ResIQ) ->
Err = xmpp:set_from_to(ResIQ, To, From),
ejabberd_router:route(To, From, Err);
process_iq_result(#iq{from = From, to = To, lang = Lang} = IQ, timeout) ->
Txt = <<"External component timeout">>,
Err = xmpp:err_internal_server_error(Txt, Lang),
ejabberd_router:route_error(To, From, IQ, Err).
-spec send_disco_queries(binary(), binary(), binary()) -> ok.
send_disco_queries(LServer, Host, NS) ->
From = jid:make(LServer),
To = jid:make(Host),
lists:foreach(
fun({Type, Node}) ->
ejabberd_local:route_iq(
From, To, #iq{type = get, from = From, to = To,
sub_els = [#disco_info{node = Node}]},
fun(#iq{type = result, sub_els = [#disco_info{} = Info]}) ->
Proc = gen_mod:get_module_proc(LServer, ?MODULE),
gen_server:cast(Proc, {disco_info, Type, Host, NS, Info});
(_) ->
ok
end)
end, [{ejabberd_local, <<(?NS_DELEGATION)/binary, "::", NS/binary>>},
{ejabberd_sm, <<(?NS_DELEGATION)/binary, ":bare:", NS/binary>>}]).
-spec disco_features(disco_acc(), jid(), jid(), binary(), binary(),
ejabberd_local | ejabberd_sm) -> disco_acc().
disco_features(Acc, _From, To, <<"">>, _Lang, Type) ->
Delegations = get_delegations(To#jid.lserver),
Features = my_features(Type) ++
lists:flatmap(
fun({{_, T}, {_, Info}}) when T == Type ->
Info#disco_info.features;
(_) ->
[]
end, dict:to_list(Delegations)),
case Acc of
empty when Features /= [] -> {result, Features};
{result, Fs} -> {result, Fs ++ Features};
_ -> Acc
end;
disco_features(Acc, _, _, _, _, _) ->
Acc.
-spec disco_identity(disco_acc(), jid(), jid(), binary(), binary(),
ejabberd_local | ejabberd_sm) -> disco_acc().
disco_identity(Acc, _From, To, <<"">>, _Lang, Type) ->
Delegations = get_delegations(To#jid.lserver),
Identities = lists:flatmap(
fun({{_, T}, {_, Info}}) when T == Type ->
Info#disco_info.identities;
(_) ->
[]
end, dict:to_list(Delegations)),
case Acc of
empty when Identities /= [] -> {result, Identities};
{result, Ids} -> {result, Ids ++ Identities};
Acc -> Acc
end.
my_features(ejabberd_local) -> [?NS_DELEGATION];
my_features(ejabberd_sm) -> [].
validate_fun() ->
fun(L) ->
lists:map(
fun({NS, Opts}) ->
Attrs = proplists:get_value(filtering, Opts, []),
Access = proplists:get_value(access, Opts, none),
{NS, Attrs, Access}
end, L)
end.

View File

@ -63,7 +63,7 @@ start_link(Host, Opts) ->
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
temporary, 1000, worker, [?MODULE]},
transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->

View File

@ -101,7 +101,7 @@
-define(AC_ALLOW_HEADERS,
{<<"Access-Control-Allow-Headers">>,
<<"Content-Type">>}).
<<"Content-Type, Authorization, X-Admin">>}).
-define(AC_MAX_AGE,
{<<"Access-Control-Max-Age">>, <<"86400">>}).
@ -118,9 +118,11 @@
%% -------------------
start(_Host, _Opts) ->
ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0),
ok.
stop(_Host) ->
ejabberd_access_permissions:unregister_permission_addon(?MODULE),
ok.
depends(_Host, _Opts) ->
@ -130,79 +132,39 @@ depends(_Host, _Opts) ->
%% basic auth
%% ----------
check_permissions(Request, Command) ->
case catch binary_to_existing_atom(Command, utf8) of
Call when is_atom(Call) ->
{ok, CommandPolicy} = ejabberd_commands:get_command_policy(Call),
check_permissions2(Request, Call, CommandPolicy);
_ ->
unauthorized_response()
end.
check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
when HTTPAuth /= undefined ->
Admin =
case lists:keysearch(<<"X-Admin">>, 1, Headers) of
{value, {_, <<"true">>}} -> true;
_ -> false
end,
Auth =
case HTTPAuth of
extract_auth(#request{auth = HTTPAuth, ip = {IP, _}}) ->
Info = case HTTPAuth of
{SJID, Pass} ->
case jid:from_string(SJID) of
#jid{user = User, server = Server} ->
#jid{luser = User, lserver = Server} ->
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
true -> {ok, {User, Server, Pass, Admin}};
false -> false
true ->
#{usr => {User, Server, <<"">>}, caller_server => Server};
false ->
{error, invalid_auth}
end;
_ ->
false
{error, invalid_auth}
end;
{oauth, Token, _} ->
case oauth_check_token(Call, Token) of
{ok, user, {User, Server}} ->
{ok, {User, Server, {oauth, Token}, Admin}};
{ok, server_admin} -> %% token whas generated using issue_token command line
{ok, admin};
false ->
false
case ejabberd_oauth:check_token(Token) of
{ok, {U, S}, Scope} ->
#{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
{false, Reason} ->
{error, Reason}
end;
_ ->
false
#{}
end,
case Auth of
{ok, A} -> {allowed, Call, A};
_ -> unauthorized_response()
case Info of
Map when is_map(Map) ->
Map#{caller_module => ?MODULE, ip => IP};
_ ->
?DEBUG("Invalid auth data: ~p", [Info]),
Info
end;
check_permissions2(_Request, Call, open) ->
{allowed, Call, noauth};
check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) ->
Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
fun(V) -> V end,
none),
Res = acl:match_rule(global, Access, IP),
case Res of
all ->
{allowed, Call, admin};
[all] ->
{allowed, Call, admin};
allow ->
{allowed, Call, admin};
Commands when is_list(Commands) ->
case lists:member(Call, Commands) of
true -> {allowed, Call, admin};
_ -> unauthorized_response()
end;
_E ->
{allowed, Call, noauth}
end;
check_permissions2(_Request, _Call, _Policy) ->
unauthorized_response().
oauth_check_token(Scope, Token) when is_atom(Scope) ->
oauth_check_token(atom_to_binary(Scope, utf8), Token);
oauth_check_token(Scope, Token) ->
ejabberd_oauth:check_token(Scope, Token).
extract_auth(#request{ip = IP}) ->
#{ip => IP, caller_module => ?MODULE}.
%% ------------------
%% command processing
@ -213,31 +175,24 @@ oauth_check_token(Scope, Token) ->
process(_, #request{method = 'POST', data = <<>>}) ->
?DEBUG("Bad Request: no data", []),
badrequest_response(<<"Missing POST data">>);
process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = Req) ->
process([Call], #request{method = 'POST', data = Data, ip = IPPort} = Req) ->
Version = get_api_version(Req),
try
Args = case jiffy:decode(Data) of
List when is_list(List) -> List;
{List} when is_list(List) -> List;
Other -> [Other]
end,
Args = extract_args(Data),
log(Call, Args, IPPort),
case check_permissions(Req, Call) of
{allowed, Cmd, Auth} ->
{Code, Result} = handle(Cmd, Auth, Args, Version, IP),
json_response(Code, jiffy:encode(Result));
%% Warning: check_permission direcly formats 401 reply if not authorized
ErrorResponse ->
ErrorResponse
end
catch _:{error,{_,invalid_json}} = _Err ->
perform_call(Call, Args, Req, Version)
catch
%% TODO We need to refactor to remove redundant error return formatting
throw:{error, unknown_command} ->
json_format({404, 44, <<"Command not found.">>});
_:{error,{_,invalid_json}} = _Err ->
?DEBUG("Bad Request: ~p", [_Err]),
badrequest_response(<<"Invalid JSON input">>);
_:_Error ->
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response()
end;
process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
process([Call], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) ->
Version = get_api_version(Req),
try
Args = case Data of
@ -245,23 +200,48 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
_ -> Data
end,
log(Call, Args, IP),
case check_permissions(Req, Call) of
{allowed, Cmd, Auth} ->
{Code, Result} = handle(Cmd, Auth, Args, Version, IP),
json_response(Code, jiffy:encode(Result));
%% Warning: check_permission direcly formats 401 reply if not authorized
ErrorResponse ->
ErrorResponse
end
catch _:_Error ->
perform_call(Call, Args, Req, Version)
catch
%% TODO We need to refactor to remove redundant error return formatting
throw:{error, unknown_command} ->
json_format({404, 44, <<"Command not found.">>});
_:_Error ->
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response()
end;
process([], #request{method = 'OPTIONS', data = <<>>}) ->
process([_Call], #request{method = 'OPTIONS', data = <<>>}) ->
{200, ?OPTIONS_HEADER, []};
process(_, #request{method = 'OPTIONS'}) ->
{400, ?OPTIONS_HEADER, []};
process(_Path, Request) ->
?DEBUG("Bad Request: no handler ~p", [Request]),
badrequest_response().
json_error(400, 40, <<"Missing command name.">>).
perform_call(Command, Args, Req, Version) ->
case catch binary_to_existing_atom(Command, utf8) of
Call when is_atom(Call) ->
case extract_auth(Req) of
{error, expired} -> invalid_token_response();
{error, not_found} -> invalid_token_response();
{error, invalid_auth} -> unauthorized_response();
{error, _} -> unauthorized_response();
Auth when is_map(Auth) ->
Result = handle(Call, Auth, Args, Version),
json_format(Result)
end;
_ ->
json_error(404, 40, <<"Endpoint not found.">>)
end.
%% Be tolerant to make API more easily usable from command-line pipe.
extract_args(<<"\n">>) -> [];
extract_args(Data) ->
case jiffy:decode(Data) of
List when is_list(List) -> List;
{List} when is_list(List) -> List;
Other -> [Other]
end.
% get API version N from last "vN" element in URL path
get_api_version(#request{path = Path}) ->
@ -282,8 +262,10 @@ get_api_version([]) ->
%% command handlers
%% ----------------
%% TODO Check accept types of request before decided format of reply.
% generic ejabberd command handler
handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
case ejabberd_commands:get_command_format(Call, Auth, Version) of
{ArgsSpec, _} when is_list(ArgsSpec) ->
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
@ -300,7 +282,7 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
[{Key, undefined}|Acc]
end, [], ArgsSpec),
try
handle2(Call, Auth, match(Args2, Spec), Version, IP)
handle2(Call, Auth, match(Args2, Spec), Version)
catch throw:not_found ->
{404, <<"not_found">>};
throw:{not_found, Why} when is_atom(Why) ->
@ -314,7 +296,9 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
throw:{not_allowed, Msg} ->
{401, iolist_to_binary(Msg)};
throw:{error, account_unprivileged} ->
{401, iolist_to_binary(<<"Unauthorized: Account Unpriviledged">>)};
{403, 31, <<"Command need to be run with admin priviledge.">>};
throw:{error, access_rules_unauthorized} ->
{403, 32, <<"AccessRules: Account associated to token does not have the right to perform the operation.">>};
throw:{invalid_parameter, Msg} ->
{400, iolist_to_binary(Msg)};
throw:{error, Why} when is_atom(Why) ->
@ -337,10 +321,15 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
{400, <<"Error">>}
end.
handle2(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
{ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
ArgsFormatted = format_args(Args, ArgsF),
ejabberd_command(Auth, Call, ArgsFormatted, Version, IP).
case ejabberd_commands:execute_command2(Call, ArgsFormatted, Auth, Version) of
{error, Error} ->
throw(Error);
Res ->
format_command_result(Call, Auth, Res, Version)
end.
get_elem_delete(A, L) ->
case proplists:get_all_values(A, L) of
@ -370,28 +359,47 @@ format_args(Args, ArgsFormat) ->
L when is_list(L) -> exit({additional_unused_args, L})
end.
format_arg({array, Elements},
{list, {ElementDefName, ElementDefFormat}})
format_arg({Elements},
{list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]} = Tuple}})
when is_list(Elements) andalso
(Tuple1S == binary orelse Tuple1S == string) ->
lists:map(fun({F1, F2}) ->
{format_arg(F1, Tuple1S), format_arg(F2, Tuple2S)};
({Val}) when is_list(Val) ->
format_arg({Val}, Tuple)
end, Elements);
format_arg(Elements,
{list, {_ElementDefName, {list, _} = ElementDefFormat}})
when is_list(Elements) ->
lists:map(fun ({struct, [{ElementName, ElementValue}]}) when
ElementDefName == ElementName ->
format_arg(ElementValue, ElementDefFormat)
end,
Elements);
format_arg({array, [{struct, Elements}]},
{list, {ElementDefName, ElementDefFormat}})
[{format_arg(Element, ElementDefFormat)}
|| Element <- Elements];
format_arg(Elements,
{list, {_ElementDefName, ElementDefFormat}})
when is_list(Elements) ->
lists:map(fun ({ElementName, ElementValue}) ->
true = ElementDefName == ElementName,
format_arg(ElementValue, ElementDefFormat)
end,
Elements);
format_arg({array, [{struct, Elements}]},
[format_arg(Element, ElementDefFormat)
|| Element <- Elements];
format_arg({[{Name, Value}]},
{tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]})
when Tuple1S == binary;
Tuple1S == string ->
{format_arg(Name, Tuple1S), format_arg(Value, Tuple2S)};
format_arg({Elements},
{tuple, ElementsDef})
when is_list(Elements) ->
FormattedList = format_args(Elements, ElementsDef),
list_to_tuple(FormattedList);
format_arg({array, Elements}, {list, ElementsDef})
F = lists:map(fun({TElName, TElDef}) ->
case lists:keyfind(atom_to_binary(TElName, latin1), 1, Elements) of
{_, Value} ->
format_arg(Value, TElDef);
_ when TElDef == binary; TElDef == string ->
<<"">>;
_ ->
?ERROR_MSG("missing field ~p in tuple ~p", [TElName, Elements]),
throw({invalid_parameter,
io_lib:format("Missing field ~w in tuple ~w", [TElName, Elements])})
end
end, ElementsDef),
list_to_tuple(F);
format_arg(Elements, {list, ElementsDef})
when is_list(Elements) and is_atom(ElementsDef) ->
[format_arg(Element, ElementsDef)
|| Element <- Elements];
@ -405,7 +413,7 @@ format_arg(undefined, string) -> <<>>;
format_arg(Arg, Format) ->
?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]),
throw({invalid_parameter,
io_lib:format("Arg ~p is not in format ~p",
io_lib:format("Arg ~w is not in format ~w",
[Arg, Format])}).
process_unicode_codepoints(Str) ->
@ -420,18 +428,6 @@ process_unicode_codepoints(Str) ->
match(Args, Spec) ->
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec].
ejabberd_command(Auth, Cmd, Args, Version, IP) ->
Access = case Auth of
admin -> [];
_ -> undefined
end,
case ejabberd_commands:execute_command(Access, Auth, Cmd, Args, Version, #{ip => IP}) of
{error, Error} ->
throw(Error);
Res ->
format_command_result(Cmd, Auth, Res, Version)
end.
format_command_result(Cmd, Auth, Result, Version) ->
{_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
case {ResultFormat, Result} of
@ -439,10 +435,12 @@ format_command_result(Cmd, Auth, Result, Version) ->
{200, 0};
{{_, rescode}, _} ->
{200, 1};
{{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok ->
{200, iolist_to_binary(Text1)};
{{_, restuple}, {_, Text2}} ->
{500, iolist_to_binary(Text2)};
{_, {error, ErrorAtom, Code, Msg}} ->
format_error_result(ErrorAtom, Code, Msg);
{{_, restuple}, {V, Text}} when V == true; V == ok ->
{200, iolist_to_binary(Text)};
{{_, restuple}, {ErrorAtom, Msg}} ->
format_error_result(ErrorAtom, 0, Msg);
{{_, {list, _}}, _V} ->
{_, L} = format_result(Result, ResultFormat),
{200, L};
@ -470,6 +468,11 @@ format_result({Code, Text}, {Name, restuple}) ->
{[{<<"res">>, Code == true orelse Code == ok},
{<<"text">>, iolist_to_binary(Text)}]}};
format_result(Code, {Name, restuple}) ->
{jlib:atom_to_binary(Name),
{[{<<"res">>, Code == true orelse Code == ok},
{<<"text">>, <<"">>}]}};
format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) ->
{jlib:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}};
@ -488,24 +491,74 @@ format_result(Tuple, {Name, {tuple, Def}}) ->
format_result(404, {_Name, _}) ->
"not_found".
format_error_result(conflict, Code, Msg) ->
{409, Code, iolist_to_binary(Msg)};
format_error_result(_ErrorAtom, Code, Msg) ->
{500, Code, iolist_to_binary(Msg)}.
unauthorized_response() ->
unauthorized_response(<<"401 Unauthorized">>).
unauthorized_response(Body) ->
json_response(401, jiffy:encode(Body)).
json_error(401, 10, <<"You are not authorized to call this command.">>).
invalid_token_response() ->
json_error(401, 10, <<"Oauth Token is invalid or expired.">>).
outofscope_response() ->
json_error(401, 11, <<"Token does not grant usage to command required scope.">>).
badrequest_response() ->
badrequest_response(<<"400 Bad Request">>).
badrequest_response(Body) ->
json_response(400, jiffy:encode(Body)).
json_format({Code, Result}) ->
json_response(Code, jiffy:encode(Result));
json_format({HTMLCode, JSONErrorCode, Message}) ->
json_error(HTMLCode, JSONErrorCode, Message).
json_response(Code, Body) when is_integer(Code) ->
{Code, ?HEADER(?CT_JSON), Body}.
%% HTTPCode, JSONCode = integers
%% message is binary
json_error(HTTPCode, JSONCode, Message) ->
{HTTPCode, ?HEADER(?CT_JSON),
jiffy:encode({[{<<"status">>, <<"error">>},
{<<"code">>, JSONCode},
{<<"message">>, Message}]})
}.
log(Call, Args, {Addr, Port}) ->
AddrS = jlib:ip_to_list({Addr, Port}),
?INFO_MSG("API call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]);
log(Call, Args, IP) ->
?INFO_MSG("API call ~s ~p (~p)", [Call, Args, IP]).
permission_addon() ->
Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
fun(V) -> V end,
none),
Rules = acl:resolve_access(Access, global),
R = lists:filtermap(
fun({V, AclRules}) when V == all; V == [all]; V == [allow]; V == allow ->
{true, {[{allow, AclRules}], {[<<"*">>], []}}};
({List, AclRules}) when is_list(List) ->
{true, {[{allow, AclRules}], {List, []}}};
(_) ->
false
end, Rules),
case R of
[] ->
none;
_ ->
{_, Res} = lists:foldl(
fun({R2, L2}, {Idx, Acc}) ->
{Idx+1, [{<<"'mod_http_api admin_ip_access' option compatibility shim ",
(integer_to_binary(Idx))/binary>>,
{[?MODULE], [{access, R2}], L2}} | Acc]}
end, {1, []}, R),
Res
end.
mod_opt_type(admin_ip_access) -> fun acl:access_rules_validator/1;
mod_opt_type(_) -> [admin_ip_access].

View File

@ -251,7 +251,7 @@ terminate(Reason, #state{server_host = ServerHost, timers = Timers}) ->
?DEBUG("Stopping upload quota process for ~s: ~p", [ServerHost, Reason]),
ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE,
handle_slot_request, 50),
lists:foreach(fun(Timer) -> timer:cancel(Timer) end, Timers).
lists:foreach(fun timer:cancel/1, Timers).
-spec code_change({down, _} | _, state(), _) -> {ok, state()}.
@ -299,7 +299,7 @@ enforce_quota(UserDir, SlotSize, _OldSize, MinSize, MaxSize) ->
{[Path | AccFiles], AccSize + Size, NewSize}
end, {[], 0, 0}, Files),
if OldSize + SlotSize > MaxSize ->
lists:foreach(fun(File) -> del_file_and_dir(File) end, DelFiles),
lists:foreach(fun del_file_and_dir/1, DelFiles),
file:del_dir(UserDir), % In case it's empty, now.
NewSize + SlotSize;
true ->
@ -314,7 +314,7 @@ delete_old_files(UserDir, CutOff) ->
[] ->
ok;
OldFiles ->
lists:foreach(fun(File) -> del_file_and_dir(File) end, OldFiles),
lists:foreach(fun del_file_and_dir/1, OldFiles),
file:del_dir(UserDir) % In case it's empty, now.
end.

View File

@ -85,7 +85,7 @@ start(Host, Opts) ->
start_supervisor(Host),
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
temporary, 1000, worker, [?MODULE]},
transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->

View File

@ -106,15 +106,12 @@ start(Host, Opts) ->
ejabberd_hooks:add(anonymous_purge_hook, Host, ?MODULE,
remove_user, 50),
case gen_mod:get_opt(assume_mam_usage, Opts,
fun(if_enabled) -> if_enabled;
(on_request) -> on_request;
(never) -> never
end, never) of
never ->
ok;
_ ->
fun(B) when is_boolean(B) -> B end, false) of
true ->
ejabberd_hooks:add(message_is_archived, Host, ?MODULE,
message_is_archived, 50)
message_is_archived, 50);
false ->
ok
end,
ejabberd_commands:register_commands(get_commands_spec()),
ok.
@ -159,15 +156,12 @@ stop(Host) ->
ejabberd_hooks:delete(anonymous_purge_hook, Host,
?MODULE, remove_user, 50),
case gen_mod:get_module_opt(Host, ?MODULE, assume_mam_usage,
fun(if_enabled) -> if_enabled;
(on_request) -> on_request;
(never) -> never
end, never) of
never ->
ok;
_ ->
fun(B) when is_boolean(B) -> B end, false) of
true ->
ejabberd_hooks:delete(message_is_archived, Host, ?MODULE,
message_is_archived, 50)
message_is_archived, 50);
false ->
ok
end,
ejabberd_commands:unregister_commands(get_commands_spec()),
ok.
@ -367,32 +361,13 @@ message_is_archived(true, _C2SState, _Peer, _JID, _Pkt) ->
true;
message_is_archived(false, C2SState, Peer,
#jid{luser = LUser, lserver = LServer}, Pkt) ->
Res = case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage,
fun(if_enabled) -> if_enabled;
(on_request) -> on_request;
(never) -> never
end, never) of
if_enabled ->
case get_prefs(LUser, LServer) of
#archive_prefs{} = P ->
{ok, P};
error ->
error
end;
on_request ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
cache_tab:lookup(archive_prefs, {LUser, LServer},
fun() ->
Mod:get_prefs(LUser, LServer)
end);
never ->
error
end,
case Res of
{ok, Prefs} ->
case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage,
fun(B) when is_boolean(B) -> B end, false) of
true ->
should_archive(strip_my_archived_tag(Pkt, LServer), LServer)
andalso should_archive_peer(C2SState, Prefs, Peer);
error ->
andalso should_archive_peer(C2SState, get_prefs(LUser, LServer),
Peer);
false ->
false
end.
@ -493,9 +468,10 @@ process_iq(LServer, #iq{from = #jid{luser = LUser}, lang = Lang,
xmpp:make_error(IQ, Err)
end.
should_archive(#message{type = T}, _LServer) when T == error; T == result ->
should_archive(#message{type = error}, _LServer) ->
false;
should_archive(#message{body = Body} = Pkt, LServer) ->
should_archive(#message{body = Body, subject = Subject,
type = Type} = Pkt, LServer) ->
case is_resent(Pkt, LServer) of
true ->
false;
@ -505,14 +481,11 @@ should_archive(#message{body = Body} = Pkt, LServer) ->
true;
no_store ->
false;
none ->
case xmpp:get_text(Body) of
<<>> ->
%% Empty body
none when Type == groupchat; Type == headline ->
false;
_ ->
true
end
none ->
xmpp:get_text(Body) /= <<>> orelse
xmpp:get_text(Subject) /= <<>>
end
end;
should_archive(_, _LServer) ->
@ -669,9 +642,15 @@ store_msg(C2SState, Pkt, LUser, LServer, Peer, Dir) ->
case should_archive_peer(C2SState, Prefs, Peer) of
true ->
US = {LUser, LServer},
case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt,
[LUser, LServer, Peer, chat, Dir]) of
drop ->
pass;
NewPkt ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
El = xmpp:encode(Pkt),
Mod:store(El, LServer, US, chat, Peer, <<"">>, Dir);
El = xmpp:encode(NewPkt),
Mod:store(El, LServer, US, chat, Peer, <<"">>, Dir)
end;
false ->
pass
end.
@ -679,11 +658,17 @@ store_msg(C2SState, Pkt, LUser, LServer, Peer, Dir) ->
store_muc(MUCState, Pkt, RoomJID, Peer, Nick) ->
case should_archive_muc(Pkt) of
true ->
LServer = MUCState#state.server_host,
{U, S, _} = jid:tolower(RoomJID),
LServer = MUCState#state.server_host,
case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt,
[U, S, Peer, groupchat, recv]) of
drop ->
pass;
NewPkt ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
El = xmpp:encode(Pkt),
Mod:store(El, LServer, {U, S}, groupchat, Peer, Nick, recv);
El = xmpp:encode(NewPkt),
Mod:store(El, LServer, {U, S}, groupchat, Peer, Nick, recv)
end;
false ->
pass
end.
@ -879,6 +864,7 @@ is_bare_copy(#jid{luser = U, lserver = S, lresource = R}, To) ->
send(Msgs, Count, IsComplete,
#iq{from = From, to = To,
sub_els = [#mam_query{id = QID, xmlns = NS}]} = IQ) ->
Hint = #hint{type = 'no-store'},
Els = lists:map(
fun({ID, _IDInt, El}) ->
#message{sub_els = [#mam_result{xmlns = NS,
@ -905,7 +891,7 @@ send(Msgs, Count, IsComplete,
fun(El) ->
ejabberd_router:route(To, From, El)
end, Els),
ejabberd_router:route(To, From, #message{sub_els = [Result]}),
ejabberd_router:route(To, From, #message{sub_els = [Result, Hint]}),
ignore
end.
@ -926,6 +912,8 @@ filter_by_max(_Msgs, _Junk) ->
-spec limit_max(rsm_set(), binary()) -> rsm_set() | undefined.
limit_max(RSM, ?NS_MAM_TMP) ->
RSM; % XEP-0313 v0.2 doesn't require clients to support RSM.
limit_max(undefined, _NS) ->
#rsm_set{max = ?DEF_PAGE_SIZE};
limit_max(#rsm_set{max = Max} = RSM, _NS) when not is_integer(Max) ->
RSM#rsm_set{max = ?DEF_PAGE_SIZE};
limit_max(#rsm_set{max = Max} = RSM, _NS) when Max > ?MAX_PAGE_SIZE ->
@ -972,10 +960,7 @@ get_commands_spec() ->
result = {res, rescode}}].
mod_opt_type(assume_mam_usage) ->
fun(if_enabled) -> if_enabled;
(on_request) -> on_request;
(never) -> never
end;
fun (B) when is_boolean(B) -> B end;
mod_opt_type(cache_life_time) ->
fun (I) when is_integer(I), I > 0 -> I end;
mod_opt_type(cache_size) ->

View File

@ -43,7 +43,7 @@ start_link(Host, Opts) ->
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
temporary, 5000, worker, [?MODULE]},
transient, 5000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->

View File

@ -71,13 +71,12 @@
server_host = <<"">> :: binary(),
access = {none, none, none, none} :: {atom(), atom(), atom(), atom()},
history_size = 20 :: non_neg_integer(),
max_rooms_discoitems = 100 :: non_neg_integer(),
default_room_opts = [] :: list(),
room_shaper = none :: shaper:shaper()}).
-define(PROCNAME, ejabberd_mod_muc).
-define(MAX_ROOMS_DISCOITEMS, 100).
-type muc_room_opts() :: [{atom(), any()}].
-callback init(binary(), gen_mod:opts()) -> any().
-callback import(binary(), #muc_room{} | #muc_registered{}) -> ok | pass.
@ -100,7 +99,7 @@ start_link(Host, Opts) ->
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
temporary, 1000, worker, [?MODULE]},
transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
@ -196,6 +195,9 @@ init([Host, Opts]) ->
HistorySize = gen_mod:get_opt(history_size, Opts,
fun(I) when is_integer(I), I>=0 -> I end,
20),
MaxRoomsDiscoItems = gen_mod:get_opt(max_rooms_discoitems, Opts,
fun(I) when is_integer(I), I>=0 -> I end,
100),
DefRoomOpts1 = gen_mod:get_opt(default_room_options, Opts,
fun(L) when is_list(L) -> L end,
[]),
@ -221,6 +223,7 @@ init([Host, Opts]) ->
public -> Bool;
public_list -> Bool;
mam -> Bool;
allow_subscription -> Bool;
password -> fun iolist_to_binary/1;
title -> fun iolist_to_binary/1;
allow_private_messages_from_visitors ->
@ -272,6 +275,7 @@ init([Host, Opts]) ->
access = {Access, AccessCreate, AccessAdmin, AccessPersistent},
default_room_opts = DefRoomOpts,
history_size = HistorySize,
max_rooms_discoitems = MaxRoomsDiscoItems,
room_shaper = RoomShaper}}.
handle_call(stop, _From, State) ->
@ -300,9 +304,10 @@ handle_info({route, From, To, Packet},
#state{host = Host, server_host = ServerHost,
access = Access, default_room_opts = DefRoomOpts,
history_size = HistorySize,
max_rooms_discoitems = MaxRoomsDiscoItems,
room_shaper = RoomShaper} = State) ->
case catch do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
From, To, Packet, DefRoomOpts) of
From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems) of
{'EXIT', Reason} ->
?ERROR_MSG("~p", [Reason]);
_ ->
@ -339,7 +344,7 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}.
%%--------------------------------------------------------------------
do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
From, To, Packet, DefRoomOpts) ->
From, To, Packet, DefRoomOpts, _MaxRoomsDiscoItems) ->
{AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent} = Access,
case acl:match_rule(ServerHost, AccessRoute, From) of
allow ->
@ -487,9 +492,13 @@ process_disco_items(#iq{type = set, lang = Lang} = IQ) ->
process_disco_items(#iq{type = get, from = From, to = To, lang = Lang,
sub_els = [#disco_items{node = Node, rsm = RSM}]} = IQ) ->
Host = To#jid.lserver,
xmpp:make_iq_result(
IQ, #disco_items{node = Node,
items = iq_disco_items(Host, From, Lang, Node, RSM)});
ServerHost = ejabberd_router:host_of_route(Host),
MaxRoomsDiscoItems = gen_mod:get_module_opt(
ServerHost, ?MODULE, max_rooms_discoitems,
fun(I) when is_integer(I), I>=0 -> I end,
100),
Items = iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, Node, RSM),
xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items});
process_disco_items(#iq{lang = Lang} = IQ) ->
Txt = <<"No module is handling this query">>,
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
@ -588,23 +597,23 @@ register_room(Host, Room, Pid) ->
end,
mnesia:transaction(F).
iq_disco_items(Host, From, Lang, <<"">>, undefined) ->
iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, <<"">>, undefined) ->
Rooms = get_vh_rooms(Host),
case erlang:length(Rooms) < ?MAX_ROOMS_DISCOITEMS of
case erlang:length(Rooms) < MaxRoomsDiscoItems of
true ->
iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang});
false ->
iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, undefined)
iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, <<"nonemptyrooms">>, undefined)
end;
iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, undefined) ->
iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, <<"nonemptyrooms">>, undefined) ->
Empty = #disco_item{jid = jid:make(<<"conference.localhost">>),
node = <<"emptyrooms">>,
name = translate:translate(Lang, <<"Empty Rooms">>)},
Query = {get_disco_item, only_non_empty, From, Lang},
[Empty | iq_disco_items_list(Host, get_vh_rooms(Host), Query)];
iq_disco_items(Host, From, Lang, <<"emptyrooms">>, undefined) ->
iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, <<"emptyrooms">>, undefined) ->
iq_disco_items_list(Host, get_vh_rooms(Host), {get_disco_item, 0, From, Lang});
iq_disco_items(Host, From, Lang, _DiscoNode, Rsm) ->
iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, _DiscoNode, Rsm) ->
{Rooms, RsmO} = get_vh_rooms(Host, Rsm),
RsmOut = jlib:rsm_encode(RsmO),
iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang}) ++ RsmOut.
@ -624,7 +633,6 @@ iq_disco_items_list(Host, Rooms, Query) ->
get_vh_rooms(_, _) ->
todo.
%% get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})->
%% AllRooms = lists:sort(get_vh_rooms(Host)),
%% Count = erlang:length(AllRooms),
%% Guard = case Direction of
@ -662,9 +670,10 @@ get_vh_rooms(_, _) ->
get_subscribed_rooms(_ServerHost, Host, From) ->
Rooms = get_vh_rooms(Host),
BareFrom = jid:remove_resource(From),
lists:flatmap(
fun(#muc_online_room{name_host = {Name, _}, pid = Pid}) ->
case gen_fsm:sync_send_all_state_event(Pid, {is_subscriber, From}) of
case gen_fsm:sync_send_all_state_event(Pid, {is_subscribed, BareFrom}) of
true -> [jid:make(Name, Host)];
false -> []
end;
@ -874,6 +883,8 @@ mod_opt_type(max_room_id) ->
fun (infinity) -> infinity;
(I) when is_integer(I), I > 0 -> I
end;
mod_opt_type(max_rooms_discoitems) ->
fun (I) when is_integer(I), I >= 0 -> I end;
mod_opt_type(regexp_room_id) ->
fun iolist_to_binary/1;
mod_opt_type(max_room_name) ->
@ -901,8 +912,8 @@ mod_opt_type(user_presence_shaper) ->
mod_opt_type(_) ->
[access, access_admin, access_create, access_persistent,
db_type, default_room_options, history_size, host,
max_room_desc, max_room_id, max_room_name, regexp_room_id,
max_user_conferences, max_users,
max_room_desc, max_room_id, max_room_name,
max_rooms_discoitems, max_user_conferences, max_users,
max_users_admin_threshold, max_users_presence,
min_message_interval, min_presence_interval,
room_shaper, user_message_shaper, user_presence_shaper].
regexp_room_id, room_shaper, user_message_shaper, user_presence_shaper].

View File

@ -13,6 +13,7 @@
-export([start/2, stop/1, depends/2, muc_online_rooms/1,
muc_unregister_nick/1, create_room/3, destroy_room/2,
create_room_with_opts/4,
create_rooms_file/1, destroy_rooms_file/1,
rooms_unused_list/2, rooms_unused_destroy/2,
get_user_rooms/2, get_room_occupants/2,
@ -20,6 +21,7 @@
change_room_option/4, get_room_options/2,
set_room_affiliation/4, get_room_affiliations/2,
web_menu_main/2, web_page_main/2, web_menu_host/3,
subscribe_room/4, unsubscribe_room/2, get_subscribers/2,
web_page_host/3, mod_opt_type/1, get_commands_spec/0]).
-include("ejabberd.hrl").
@ -87,6 +89,18 @@ get_commands_spec() ->
module = ?MODULE, function = create_rooms_file,
args = [{file, string}],
result = {res, rescode}},
#ejabberd_commands{name = create_room_with_opts, tags = [muc_room],
desc = "Create a MUC room name@service in host with given options",
module = ?MODULE, function = create_room_with_opts,
args = [{name, binary}, {service, binary},
{host, binary},
{options, {list,
{option, {tuple,
[{name, binary},
{value, binary}
]}}
}}],
result = {res, rescode}},
#ejabberd_commands{name = destroy_rooms_file, tags = [muc],
desc = "Destroy the rooms indicated in file",
longdesc = "Provide one room JID per line.",
@ -151,7 +165,22 @@ get_commands_spec() ->
{value, string}
]}}
}}},
#ejabberd_commands{name = subscribe_room, tags = [muc_room],
desc = "Subscribe to a MUC conference",
module = ?MODULE, function = subscribe_room,
args = [{user, binary}, {nick, binary}, {room, binary},
{nodes, binary}],
result = {nodes, {list, {node, string}}}},
#ejabberd_commands{name = unsubscribe_room, tags = [muc_room],
desc = "Unsubscribe from a MUC conference",
module = ?MODULE, function = unsubscribe_room,
args = [{user, binary}, {room, binary}],
result = {res, rescode}},
#ejabberd_commands{name = get_subscribers, tags = [muc_room],
desc = "List subscribers of a MUC conference",
module = ?MODULE, function = get_subscribers,
args = [{name, binary}, {service, binary}],
result = {subscribers, {list, {jid, string}}}},
#ejabberd_commands{name = set_room_affiliation, tags = [muc_room],
desc = "Change an affiliation in a MUC room",
module = ?MODULE, function = set_room_affiliation,
@ -400,15 +429,23 @@ prepare_room_info(Room_info) ->
%% ok | error
%% @doc Create a room immediately with the default options.
create_room(Name1, Host1, ServerHost) ->
Name = jid:nodeprep(Name1),
Host = jid:nodeprep(Host1),
create_room_with_opts(Name1, Host1, ServerHost, []).
create_room_with_opts(Name1, Host1, ServerHost, CustomRoomOpts) ->
true = (error /= (Name = jid:nodeprep(Name1))),
true = (error /= (Host = jid:nodeprep(Host1))),
%% Get the default room options from the muc configuration
DefRoomOpts = gen_mod:get_module_opt(ServerHost, mod_muc,
default_room_options, fun(X) -> X end, []),
%% Change default room options as required
FormattedRoomOpts = [format_room_option(Opt, Val) || {Opt, Val}<-CustomRoomOpts],
RoomOpts = lists:ukeymerge(1,
lists:keysort(1, FormattedRoomOpts),
lists:keysort(1, DefRoomOpts)),
%% Store the room on the server, it is not started yet though at this point
mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts),
mod_muc:store_room(ServerHost, Host, Name, RoomOpts),
%% Get all remaining mod_muc parameters that might be utilized
Access = gen_mod:get_module_opt(ServerHost, mod_muc, access, fun(X) -> X end, all),
@ -429,7 +466,7 @@ create_room(Name1, Host1, ServerHost) ->
Name,
HistorySize,
RoomShaper,
DefRoomOpts),
RoomOpts),
{atomic, ok} = register_room(Host, Name, Pid),
ok;
_ ->
@ -477,7 +514,7 @@ destroy_room({N, H, SH}) ->
%% The file encoding must be UTF-8
destroy_rooms_file(Filename) ->
{ok, F} = file:open(Filename, [read, binary]),
{ok, F} = file:open(Filename, [read]),
RJID = read_room(F),
Rooms = read_rooms(F, RJID, []),
file:close(F),
@ -496,7 +533,7 @@ read_room(F) ->
eof -> eof;
String ->
case io_lib:fread("~s", String) of
{ok, [RoomJID], _} -> split_roomjid(RoomJID);
{ok, [RoomJID], _} -> split_roomjid(list_to_binary(RoomJID));
{error, What} ->
io:format("Parse error: what: ~p~non the line: ~p~n~n", [What, String])
end
@ -514,7 +551,7 @@ split_roomjid(RoomJID) ->
%%----------------------------
create_rooms_file(Filename) ->
{ok, F} = file:open(Filename, [read, binary]),
{ok, F} = file:open(Filename, [read]),
RJID = read_room(F),
Rooms = read_rooms(F, RJID, []),
file:close(F),
@ -748,12 +785,20 @@ send_direct_invitation(FromJid, UserJid, XmlEl) ->
%% the option to change (for example title or max_users),
%% and the value to assign to the new option.
%% For example:
%% change_room_option("testroom", "conference.localhost", "title", "Test Room")
change_room_option(Name, Service, Option, Value) when is_atom(Option) ->
Pid = get_room_pid(Name, Service),
{ok, _} = change_room_option(Pid, Option, Value),
ok;
%% change_room_option(<<"testroom">>, <<"conference.localhost">>, <<"title">>, <<"Test Room">>)
change_room_option(Name, Service, OptionString, ValueString) ->
case get_room_pid(Name, Service) of
room_not_found ->
room_not_found;
Pid ->
{Option, Value} = format_room_option(OptionString, ValueString),
Config = get_room_config(Pid),
Config2 = change_option(Option, Value, Config),
{ok, _} = gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}),
ok
end.
format_room_option(OptionString, ValueString) ->
Option = jlib:binary_to_atom(OptionString),
Value = case Option of
title -> ValueString;
@ -764,12 +809,7 @@ change_room_option(Name, Service, OptionString, ValueString) ->
max_users -> binary_to_integer(ValueString);
_ -> jlib:binary_to_atom(ValueString)
end,
change_room_option(Name, Service, Option, Value).
change_room_option(Pid, Option, Value) ->
Config = get_room_config(Pid),
Config2 = change_option(Option, Value, Config),
gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}).
{Option, Value}.
%% @doc Get the Pid of an existing MUC room, or 'room_not_found'.
get_room_pid(Name, Service) ->
@ -789,6 +829,7 @@ change_option(Option, Value, Config) ->
allow_private_messages -> Config#config{allow_private_messages = Value};
allow_private_messages_from_visitors -> Config#config{allow_private_messages_from_visitors = Value};
allow_query_users -> Config#config{allow_query_users = Value};
allow_subscription -> Config#config{allow_subscription = Value};
allow_user_invites -> Config#config{allow_user_invites = Value};
allow_visitor_nickchange -> Config#config{allow_visitor_nickchange = Value};
allow_visitor_status -> Config#config{allow_visitor_status = Value};
@ -884,6 +925,74 @@ set_room_affiliation(Name, Service, JID, AffiliationString) ->
error
end.
%%%
%%% MUC Subscription
%%%
subscribe_room(_User, Nick, _Room, _Nodes) when Nick == <<"">> ->
throw({error, "Nickname must be set"});
subscribe_room(User, Nick, Room, Nodes) ->
NodeList = re:split(Nodes, "\\h*,\\h*"),
case jid:from_string(Room) of
#jid{luser = Name, lserver = Host} when Name /= <<"">> ->
case jid:from_string(User) of
error ->
throw({error, "Malformed user JID"});
#jid{lresource = <<"">>} ->
throw({error, "User's JID should have a resource"});
UserJID ->
case get_room_pid(Name, Host) of
Pid when is_pid(Pid) ->
case gen_fsm:sync_send_all_state_event(
Pid,
{muc_subscribe, UserJID, Nick, NodeList}) of
{ok, SubscribedNodes} ->
SubscribedNodes;
{error, Reason} ->
throw({error, binary_to_list(Reason)})
end;
_ ->
throw({error, "The room does not exist"})
end
end;
_ ->
throw({error, "Malformed room JID"})
end.
unsubscribe_room(User, Room) ->
case jid:from_string(Room) of
#jid{luser = Name, lserver = Host} when Name /= <<"">> ->
case jid:from_string(User) of
error ->
throw({error, "Malformed user JID"});
UserJID ->
case get_room_pid(Name, Host) of
Pid when is_pid(Pid) ->
case gen_fsm:sync_send_all_state_event(
Pid,
{muc_unsubscribe, UserJID}) of
ok ->
ok;
{error, Reason} ->
throw({error, binary_to_list(Reason)})
end;
_ ->
throw({error, "The room does not exist"})
end
end;
_ ->
throw({error, "Malformed room JID"})
end.
get_subscribers(Name, Host) ->
case get_room_pid(Name, Host) of
Pid when is_pid(Pid) ->
{ok, JIDList} = gen_fsm:sync_send_all_state_event(Pid, get_subscribers),
[jid:to_string(jid:remove_resource(J)) || J <- JIDList];
_ ->
throw({error, "The room does not exist"})
end.
make_opts(StateData) ->
Config = StateData#state.config,
[

View File

@ -81,7 +81,7 @@ start_link(Host, Opts) ->
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
temporary, 1000, worker, [?MODULE]},
transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->

View File

@ -142,6 +142,7 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) ->
normal_state({route, From, <<"">>,
#message{type = Type, lang = Lang} = Packet}, StateData) ->
case is_user_online(From, StateData) orelse
is_subscriber(From, StateData) orelse
is_user_allowed_message_nonparticipant(From, StateData) of
true when Type == groupchat ->
Activity = get_user_activity(From, StateData),
@ -372,7 +373,8 @@ normal_state({route, From, ToNick,
{next_state, normal_state, StateData};
continue_delivery ->
case {(StateData#state.config)#config.allow_private_messages,
is_user_online(From, StateData)} of
is_user_online(From, StateData) orelse
is_subscriber(From, StateData)} of
{true, true} when Type == groupchat ->
ErrText = <<"It is not allowed to send private messages "
"of type \"groupchat\"">>,
@ -397,9 +399,7 @@ normal_state({route, From, ToNick,
PmFromVisitors == anyone;
(PmFromVisitors == moderators) and
DstIsModerator ->
{ok, #user{nick = FromNick}} =
(?DICT):find(jid:tolower(From),
StateData#state.users),
{FromNick, _} = get_participant_data(From, StateData),
FromNickJID =
jid:replace_resource(StateData#state.jid,
FromNick),
@ -476,7 +476,7 @@ handle_event({service_message, Msg}, _StateName,
MessagePkt = #message{type = groupchat, body = xmpp:mk_text(Msg)},
send_wrapped_multiple(
StateData#state.jid,
StateData#state.users,
get_users_and_subscribers(StateData),
MessagePkt,
?NS_MUCSUB_NODES_MESSAGES,
StateData),
@ -538,8 +538,59 @@ handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData
NSD ->
{reply, {ok, NSD}, StateName, NSD}
end;
handle_sync_event({is_subscriber, From}, _From, StateName, StateData) ->
{reply, is_subscriber(From, StateData), StateName, StateData};
handle_sync_event(get_subscribers, _From, StateName, StateData) ->
JIDs = lists:map(fun jid:make/1,
?DICT:fetch_keys(StateData#state.subscribers)),
{reply, {ok, JIDs}, StateName, StateData};
handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From,
StateName, StateData) ->
IQ = #iq{type = set, id = randoms:get_string(),
from = From, sub_els = [#muc_subscribe{nick = Nick,
events = Nodes}]},
Config = StateData#state.config,
CaptchaRequired = Config#config.captcha_protected,
PasswordProtected = Config#config.password_protected,
TmpConfig = Config#config{captcha_protected = false,
password_protected = false},
TmpState = StateData#state{config = TmpConfig},
case process_iq_mucsub(From, IQ, TmpState) of
{result, #muc_subscribe{events = NewNodes}, NewState} ->
NewConfig = (NewState#state.config)#config{
captcha_protected = CaptchaRequired,
password_protected = PasswordProtected},
{reply, {ok, NewNodes}, StateName,
NewState#state{config = NewConfig}};
{ignore, NewState} ->
NewConfig = (NewState#state.config)#config{
captcha_protected = CaptchaRequired,
password_protected = PasswordProtected},
{reply, {error, <<"Requrest is ignored">>},
NewState#state{config = NewConfig}};
{error, Err, NewState} ->
NewConfig = (NewState#state.config)#config{
captcha_protected = CaptchaRequired,
password_protected = PasswordProtected},
{reply, {error, get_error_text(Err)}, StateName,
NewState#state{config = NewConfig}};
{error, Err} ->
{reply, {error, get_error_text(Err)}, StateName, StateData}
end;
handle_sync_event({muc_unsubscribe, From}, _From, StateName, StateData) ->
IQ = #iq{type = set, id = randoms:get_string(),
from = From, sub_els = [#muc_unsubscribe{}]},
case process_iq_mucsub(From, IQ, StateData) of
{result, _, NewState} ->
{reply, ok, StateName, NewState};
{ignore, NewState} ->
{reply, {error, <<"Requrest is ignored">>}, NewState};
{error, Err, NewState} ->
{reply, {error, get_error_text(Err)}, StateName, NewState};
{error, Err} ->
{reply, {error, get_error_text(Err)}, StateName, StateData}
end;
handle_sync_event({is_subscribed, From}, _From, StateName, StateData) ->
IsSubs = ?DICT:is_key(jid:split(From), StateData#state.subscribers),
{reply, IsSubs, StateName, StateData};
handle_sync_event(_Event, _From, StateName,
StateData) ->
Reply = ok, {reply, Reply, StateName, StateData}.
@ -654,7 +705,7 @@ terminate(Reason, _StateName, StateData) ->
end,
tab_remove_online_user(LJID, StateData)
end,
[], StateData#state.users),
[], get_users_and_subscribers(StateData)),
add_to_log(room_existence, stopped, StateData),
mod_muc:room_destroyed(StateData#state.host, StateData#state.room, self(),
StateData#state.server_host),
@ -669,11 +720,12 @@ route(Pid, From, ToNick, Packet) ->
-spec process_groupchat_message(jid(), message(), state()) -> fsm_next().
process_groupchat_message(From, #message{lang = Lang} = Packet, StateData) ->
case is_user_online(From, StateData) orelse
IsSubscriber = is_subscriber(From, StateData),
case is_user_online(From, StateData) orelse IsSubscriber orelse
is_user_allowed_message_nonparticipant(From, StateData)
of
true ->
{FromNick, Role, IsSubscriber} = get_participant_data(From, StateData),
{FromNick, Role} = get_participant_data(From, StateData),
if (Role == moderator) or (Role == participant) or IsSubscriber or
((StateData#state.config)#config.moderated == false) ->
Subject = check_subject(Packet),
@ -682,6 +734,7 @@ process_groupchat_message(From, #message{lang = Lang} = Packet, StateData) ->
_ ->
case
can_change_subject(Role,
IsSubscriber,
StateData)
of
true ->
@ -716,7 +769,7 @@ process_groupchat_message(From, #message{lang = Lang} = Packet, StateData) ->
end,
send_wrapped_multiple(
jid:replace_resource(StateData#state.jid, FromNick),
StateData#state.users,
get_users_and_subscribers(StateData),
NewPacket, Node, NewStateData1),
NewStateData2 = case has_body_or_subject(NewPacket) of
true ->
@ -907,14 +960,21 @@ is_user_allowed_message_nonparticipant(JID,
%% @doc Get information of this participant, or default values.
%% If the JID is not a participant, return values for a service message.
-spec get_participant_data(jid(), state()) -> {binary(), role(), boolean()}.
-spec get_participant_data(jid(), state()) -> {binary(), role()}.
get_participant_data(From, StateData) ->
case (?DICT):find(jid:tolower(From),
StateData#state.users)
of
{ok, #user{nick = FromNick, role = Role, is_subscriber = IsSubscriber}} ->
{FromNick, Role, IsSubscriber};
error -> {<<"">>, moderator, false}
{ok, #user{nick = FromNick, role = Role}} ->
{FromNick, Role};
error ->
case ?DICT:find(jid:tolower(jid:remove_resource(From)),
StateData#state.subscribers) of
{ok, #subscriber{nick = FromNick}} ->
{FromNick, none};
error ->
{<<"">>, moderator}
end
end.
-spec process_presence(jid(), binary(), presence(), state()) -> fsm_transition().
@ -979,19 +1039,8 @@ do_process_presence(From, Nick, #presence{type = available, lang = Lang} = Packe
From, Packet, Err),
StateData;
_ ->
case is_initial_presence(From, StateData) of
true ->
subscriber_becomes_available(
From, Nick, Packet, StateData);
false ->
change_nick(From, Nick, StateData)
end
end;
_NotNickChange ->
case is_initial_presence(From, StateData) of
true ->
subscriber_becomes_available(
From, Nick, Packet, StateData);
false ->
Stanza = maybe_strip_status_from_presence(
From, Packet, StateData),
@ -1000,11 +1049,9 @@ do_process_presence(From, Nick, #presence{type = available, lang = Lang} = Packe
send_new_presence(From, NewState, StateData),
NewState
end
end
end;
do_process_presence(From, Nick, #presence{type = unavailable} = Packet,
StateData) ->
IsSubscriber = is_subscriber(From, StateData),
NewPacket = case {(StateData#state.config)#config.allow_visitor_status,
is_visitor(From, StateData)} of
{false, true} ->
@ -1017,7 +1064,7 @@ do_process_presence(From, Nick, #presence{type = unavailable} = Packet,
_ -> send_new_presence(From, NewState, StateData)
end,
Reason = xmpp:get_text(NewPacket#presence.status),
remove_online_user(From, NewState, IsSubscriber, Reason);
remove_online_user(From, NewState, Reason);
do_process_presence(From, _Nick, #presence{type = error, lang = Lang} = Packet,
StateData) ->
ErrorText = <<"It is not allowed to send error messages to the"
@ -1036,24 +1083,11 @@ maybe_strip_status_from_presence(From, Packet, StateData) ->
_Allowed -> Packet
end.
-spec subscriber_becomes_available(jid(), binary(), presence(),
state()) -> state().
subscriber_becomes_available(From, Nick, Packet, StateData) ->
Stanza = maybe_strip_status_from_presence(From, Packet, StateData),
State1 = add_user_presence(From, Stanza, StateData),
Aff = get_affiliation(From, State1),
Role = get_default_role(Aff, State1),
State2 = set_role(From, Role, State1),
State3 = set_nick(From, Nick, State2),
send_existing_presences(From, State3),
send_initial_presence(From, State3, StateData),
State3.
-spec close_room_if_temporary_and_empty(state()) -> fsm_transition().
close_room_if_temporary_and_empty(StateData1) ->
case not (StateData1#state.config)#config.persistent
andalso (?DICT):size(StateData1#state.users) == 0
of
andalso (?DICT):size(StateData1#state.subscribers) == 0 of
true ->
?INFO_MSG("Destroyed MUC room ~s because it's temporary "
"and empty",
@ -1063,6 +1097,32 @@ close_room_if_temporary_and_empty(StateData1) ->
_ -> {next_state, normal_state, StateData1}
end.
get_users_and_subscribers(StateData) ->
OnlineSubscribers = ?DICT:fold(
fun(LJID, _, Acc) ->
LBareJID = jid:remove_resource(LJID),
case is_subscriber(LBareJID, StateData) of
true ->
?SETS:add_element(LBareJID, Acc);
false ->
Acc
end
end, ?SETS:new(), StateData#state.users),
?DICT:fold(
fun(LBareJID, #subscriber{nick = Nick}, Acc) ->
case ?SETS:is_element(LBareJID, OnlineSubscribers) of
false ->
?DICT:store(LBareJID,
#user{jid = jid:make(LBareJID),
nick = Nick,
role = none,
last_presence = undefined},
Acc);
true ->
Acc
end
end, StateData#state.users, StateData#state.subscribers).
-spec is_user_online(jid(), state()) -> boolean().
is_user_online(JID, StateData) ->
LJID = jid:tolower(JID),
@ -1070,13 +1130,8 @@ is_user_online(JID, StateData) ->
-spec is_subscriber(jid(), state()) -> boolean().
is_subscriber(JID, StateData) ->
LJID = jid:tolower(JID),
case (?DICT):find(LJID, StateData#state.users) of
{ok, #user{is_subscriber = IsSubscriber}} ->
IsSubscriber;
_ ->
false
end.
LJID = jid:tolower(jid:remove_resource(JID)),
(?DICT):is_key(LJID, StateData#state.subscribers).
%% Check if the user is occupant of the room, or at least is an admin or owner.
-spec is_occupant_or_admin(jid(), state()) -> boolean().
@ -1201,6 +1256,14 @@ get_error_condition(#stanza_error{reason = Reason}) ->
get_error_condition(undefined) ->
"undefined".
get_error_text(Error) ->
case fxml:get_subtag_with_xmlns(Error, <<"text">>, ?NS_STANZAS) of
#xmlel{} = Tag ->
fxml:get_tag_cdata(Tag);
false ->
<<"">>
end.
-spec make_reason(stanza(), jid(), state(), binary()) -> binary().
make_reason(Packet, From, StateData, Reason1) ->
{ok, #user{nick = FromNick}} = (?DICT):find(jid:tolower(From), StateData#state.users),
@ -1210,14 +1273,13 @@ make_reason(Packet, From, StateData, Reason1) ->
-spec expulse_participant(stanza(), jid(), state(), binary()) ->
state().
expulse_participant(Packet, From, StateData, Reason1) ->
IsSubscriber = is_subscriber(From, StateData),
Reason2 = make_reason(Packet, From, StateData, Reason1),
NewState = add_user_presence_un(From,
#presence{type = unavailable,
status = xmpp:mk_text(Reason2)},
StateData),
send_new_presence(From, NewState, StateData),
remove_online_user(From, NewState, IsSubscriber).
remove_online_user(From, NewState).
-spec set_affiliation(jid(), affiliation(), state()) -> state().
set_affiliation(JID, Affiliation, StateData) ->
@ -1514,8 +1576,7 @@ prepare_room_queue(StateData) ->
end.
-spec update_online_user(jid(), #user{}, state()) -> state().
update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
is_subscriber = IsSubscriber} = User, StateData) ->
update_online_user(JID, #user{nick = Nick} = User, StateData) ->
LJID = jid:tolower(JID),
Nicks1 = case (?DICT):find(LJID, StateData#state.users) of
{ok, #user{nick = OldNick}} ->
@ -1534,9 +1595,7 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
[LJID], Nicks1),
Users = (?DICT):update(LJID,
fun(U) ->
U#user{nick = Nick,
subscriptions = Nodes,
is_subscriber = IsSubscriber}
U#user{nick = Nick}
end, User, StateData#state.users),
NewStateData = StateData#state{users = Users, nicks = Nicks},
case {?DICT:find(LJID, StateData#state.users),
@ -1548,35 +1607,32 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
end,
NewStateData.
-spec add_online_user(jid(), binary(), role(), boolean(), [binary()], state()) -> state().
add_online_user(JID, Nick, Role, IsSubscriber, Nodes, StateData) ->
set_subscriber(JID, Nick, Nodes, StateData) ->
BareJID = jid:remove_resource(JID),
LBareJID = jid:tolower(BareJID),
Subscribers = ?DICT:store(LBareJID,
#subscriber{jid = BareJID,
nick = Nick,
nodes = Nodes},
StateData#state.subscribers),
Nicks = ?DICT:store(Nick, [LBareJID], StateData#state.subscriber_nicks),
NewStateData = StateData#state{subscribers = Subscribers,
subscriber_nicks = Nicks},
store_room(NewStateData),
NewStateData.
-spec add_online_user(jid(), binary(), role(), state()) -> state().
add_online_user(JID, Nick, Role, StateData) ->
tab_add_online_user(JID, StateData),
User = #user{jid = JID, nick = Nick, role = Role,
is_subscriber = IsSubscriber, subscriptions = Nodes},
StateData1 = update_online_user(JID, User, StateData),
if IsSubscriber ->
store_room(StateData1);
true ->
ok
end,
StateData1.
User = #user{jid = JID, nick = Nick, role = Role},
update_online_user(JID, User, StateData).
-spec remove_online_user(jid(), state(), boolean()) -> state().
remove_online_user(JID, StateData, IsSubscriber) ->
remove_online_user(JID, StateData, IsSubscriber, <<"">>).
-spec remove_online_user(jid(), state()) -> state().
remove_online_user(JID, StateData) ->
remove_online_user(JID, StateData, <<"">>).
-spec remove_online_user(jid(), state(), boolean(), binary()) -> state().
remove_online_user(JID, StateData, _IsSubscriber = true, _Reason) ->
LJID = jid:tolower(JID),
Users = case (?DICT):find(LJID, StateData#state.users) of
{ok, U} ->
(?DICT):store(LJID, U#user{last_presence = undefined},
StateData#state.users);
error ->
StateData#state.users
end,
StateData#state{users = Users};
remove_online_user(JID, StateData, _IsSubscriber, Reason) ->
-spec remove_online_user(jid(), state(), binary()) -> state().
remove_online_user(JID, StateData, Reason) ->
LJID = jid:tolower(JID),
{ok, #user{nick = Nick}} = (?DICT):find(LJID,
StateData#state.users),
@ -1636,7 +1692,10 @@ add_user_presence_un(JID, Presence, StateData) ->
%% Return jid record.
-spec find_jids_by_nick(binary(), state()) -> [jid()].
find_jids_by_nick(Nick, StateData) ->
case (?DICT):find(Nick, StateData#state.nicks) of
Nicks = ?DICT:merge(fun(_, Val, _) -> Val end,
StateData#state.nicks,
StateData#state.subscriber_nicks),
case (?DICT):find(Nick, Nicks) of
{ok, [User]} -> [jid:make(User)];
{ok, Users} -> [jid:make(LJID) || LJID <- Users];
error -> []
@ -1704,7 +1763,14 @@ is_nick_change(JID, Nick, StateData) ->
-spec nick_collision(jid(), binary(), state()) -> boolean().
nick_collision(User, Nick, StateData) ->
UserOfNick = find_jid_by_nick(Nick, StateData),
UserOfNick = case find_jid_by_nick(Nick, StateData) of
false ->
case ?DICT:find(Nick, StateData#state.subscriber_nicks) of
{ok, [J]} -> J;
error -> false
end;
J -> J
end,
(UserOfNick /= false andalso
jid:remove_resource(jid:tolower(UserOfNick))
/= jid:remove_resource(jid:tolower(User))).
@ -1819,8 +1885,7 @@ add_new_user(From, Nick, Packet, StateData) ->
NewState = add_user_presence(
From, Packet,
add_online_user(From, Nick, Role,
IsSubscribeRequest,
Nodes, StateData)),
StateData)),
send_existing_presences(From, NewState),
send_initial_presence(From, NewState, StateData),
History = get_history(Nick, Packet, NewState),
@ -1828,9 +1893,7 @@ add_new_user(From, Nick, Packet, StateData) ->
send_subject(From, StateData),
NewState;
true ->
add_online_user(From, Nick, none,
IsSubscribeRequest,
Nodes, StateData)
set_subscriber(From, Nick, Nodes, StateData)
end,
ResultState =
case NewStateData#state.just_created of
@ -2020,16 +2083,6 @@ presence_broadcast_allowed(JID, StateData) ->
Role = get_role(JID, StateData),
lists:member(Role, (StateData#state.config)#config.presence_broadcast).
-spec is_initial_presence(jid(), state()) -> boolean().
is_initial_presence(From, StateData) ->
LJID = jid:tolower(From),
case (?DICT):find(LJID, StateData#state.users) of
{ok, #user{last_presence = Pres}} when Pres /= undefined ->
false;
_ ->
true
end.
-spec send_initial_presence(jid(), state(), state()) -> ok.
send_initial_presence(NJID, StateData, OldStateData) ->
send_new_presence1(NJID, <<"">>, true, StateData, OldStateData).
@ -2126,7 +2179,7 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
true ->
[{LNJID, UserInfo}];
false ->
(?DICT):to_list(StateData#state.users)
(?DICT):to_list(get_users_and_subscribers(StateData))
end,
lists:foreach(
fun({LUJID, Info}) ->
@ -2158,7 +2211,7 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
Info#user.jid, Packet, Node1, StateData),
Type = xmpp:get_type(Packet),
IsSubscriber = Info#user.is_subscriber,
IsSubscriber = is_subscriber(Info#user.jid, StateData),
IsOccupant = Info#user.last_presence /= undefined,
if (IsSubscriber and not IsOccupant) and
(IsInitialPresence or (Type == unavailable)) ->
@ -2306,20 +2359,21 @@ send_nick_changing(JID, OldNick, StateData,
(_) ->
ok
end,
(?DICT):to_list(StateData#state.users)).
?DICT:to_list(get_users_and_subscribers(StateData))).
-spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok.
maybe_send_affiliation(JID, Affiliation, StateData) ->
LJID = jid:tolower(JID),
Users = get_users_and_subscribers(StateData),
IsOccupant = case LJID of
{LUser, LServer, <<"">>} ->
not (?DICT):is_empty(
(?DICT):filter(fun({U, S, _}, _) ->
U == LUser andalso
S == LServer
end, StateData#state.users));
end, Users));
{_LUser, _LServer, _LResource} ->
(?DICT):is_key(LJID, StateData#state.users)
(?DICT):is_key(LJID, Users)
end,
case IsOccupant of
true ->
@ -2335,19 +2389,19 @@ send_affiliation(JID, Affiliation, StateData) ->
role = none},
Message = #message{id = randoms:get_string(),
sub_els = [#muc_user{items = [Item]}]},
Users = get_users_and_subscribers(StateData),
Recipients = case (StateData#state.config)#config.anonymous of
true ->
(?DICT):filter(fun(_, #user{role = moderator}) ->
true;
(_, _) ->
false
end, StateData#state.users);
end, Users);
false ->
StateData#state.users
Users
end,
send_multiple(StateData#state.jid,
StateData#state.server_host,
Recipients, Message).
send_wrapped_multiple(StateData#state.jid, Recipients, Message,
?NS_MUCSUB_NODES_AFFILIATIONS, StateData).
-spec status_codes(boolean(), boolean(), state()) -> [pos_integer()].
status_codes(IsInitialPresence, _IsSelfPresence = true, StateData) ->
@ -2452,11 +2506,11 @@ check_subject(#message{subject = [_|_] = Subj, body = [],
check_subject(_) ->
false.
-spec can_change_subject(role(), state()) -> boolean().
can_change_subject(Role, StateData) ->
-spec can_change_subject(role(), boolean(), state()) -> boolean().
can_change_subject(Role, IsSubscriber, StateData) ->
case (StateData#state.config)#config.allow_change_subj
of
true -> Role == moderator orelse Role == participant;
true -> Role == moderator orelse Role == participant orelse IsSubscriber == true;
_ -> Role == moderator
end.
@ -2925,7 +2979,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick),
send_wrapped(RoomJIDNick, Info#user.jid, Packet,
?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
IsSubscriber = Info#user.is_subscriber,
IsSubscriber = is_subscriber(Info#user.jid, StateData),
IsOccupant = Info#user.last_presence /= undefined,
if (IsSubscriber and not IsOccupant) ->
send_wrapped(RoomJIDNick, Info#user.jid, Packet,
@ -2934,7 +2988,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
ok
end
end,
(?DICT):to_list(StateData#state.users)).
(?DICT):to_list(get_users_and_subscribers(StateData))).
-spec get_actor_nick(binary() | jid(), state()) -> binary().
get_actor_nick(<<"">>, _StateData) ->
@ -3301,7 +3355,7 @@ send_config_change_info(New, #state{config = Old} = StateData) ->
id = randoms:get_string(),
sub_els = [#muc_user{status_codes = Codes}]},
send_wrapped_multiple(StateData#state.jid,
StateData#state.users,
get_users_and_subscribers(StateData),
Message,
?NS_MUCSUB_NODES_CONFIG,
StateData);
@ -3321,7 +3375,7 @@ remove_nonmembers(StateData) ->
_ -> SD
end
end,
StateData, (?DICT):to_list(StateData#state.users)).
StateData, (?DICT):to_list(get_users_and_subscribers(StateData))).
-spec set_opts([{atom(), any()}], state()) -> state().
set_opts([], StateData) -> StateData;
@ -3443,14 +3497,17 @@ set_opts([{Opt, Val} | Opts], StateData) ->
StateData#state{config =
(StateData#state.config)#config{allow_subscription = Val}};
subscribers ->
lists:foldl(
fun({JID, Nick, Nodes}, State) ->
User = #user{jid = JID, nick = Nick,
subscriptions = Nodes,
is_subscriber = true,
role = none},
update_online_user(JID, User, State)
end, StateData, Val);
Subscribers = lists:foldl(
fun({JID, Nick, Nodes}, Acc) ->
BareJID = jid:remove_resource(JID),
?DICT:store(
jid:tolower(BareJID),
#subscriber{jid = BareJID,
nick = Nick,
nodes = Nodes},
Acc)
end, ?DICT:new(), Val),
StateData#state{subscribers = Subscribers};
affiliations ->
StateData#state{affiliations = (?DICT):from_list(Val)};
subject -> StateData#state{subject = Val};
@ -3466,12 +3523,11 @@ set_opts([{Opt, Val} | Opts], StateData) ->
make_opts(StateData) ->
Config = StateData#state.config,
Subscribers = (?DICT):fold(
fun(_LJID, #user{is_subscriber = true} = User, Acc) ->
[{User#user.jid, User#user.nick,
User#user.subscriptions}|Acc];
(_, _, Acc) ->
Acc
end, [], StateData#state.users),
fun(_LJID, Sub, Acc) ->
[{Sub#subscriber.jid,
Sub#subscriber.nick,
Sub#subscriber.nodes}|Acc]
end, [], StateData#state.subscribers),
[?MAKE_CONFIG_OPT(#config.title), ?MAKE_CONFIG_OPT(#config.description),
?MAKE_CONFIG_OPT(#config.allow_change_subj),
?MAKE_CONFIG_OPT(#config.allow_query_users),
@ -3490,6 +3546,7 @@ make_opts(StateData) ->
?MAKE_CONFIG_OPT(#config.password), ?MAKE_CONFIG_OPT(#config.anonymous),
?MAKE_CONFIG_OPT(#config.logging), ?MAKE_CONFIG_OPT(#config.max_users),
?MAKE_CONFIG_OPT(#config.allow_voice_requests),
?MAKE_CONFIG_OPT(#config.allow_subscription),
?MAKE_CONFIG_OPT(#config.mam),
?MAKE_CONFIG_OPT(#config.voice_request_min_interval),
?MAKE_CONFIG_OPT(#config.vcard),
@ -3517,7 +3574,7 @@ destroy_room(DEl, StateData) ->
Info#user.jid, Packet,
?NS_MUCSUB_NODES_CONFIG, StateData)
end,
(?DICT):to_list(StateData#state.users)),
(?DICT):to_list(get_users_and_subscribers(StateData))),
case (StateData#state.config)#config.persistent of
true ->
mod_muc:forget_room(StateData#state.server_host,
@ -3655,9 +3712,9 @@ process_iq_mucsub(From,
#iq{type = set, lang = Lang,
sub_els = [#muc_subscribe{nick = Nick}]} = Packet,
StateData) ->
LJID = jid:tolower(From),
case (?DICT):find(LJID, StateData#state.users) of
{ok, #user{role = Role, nick = Nick1}} when Nick1 /= Nick ->
LBareJID = jid:tolower(jid:remove_resource(From)),
case (?DICT):find(LBareJID, StateData#state.subscribers) of
{ok, #subscriber{nick = Nick1}} when Nick1 /= Nick ->
Nodes = get_subscription_nodes(Packet),
case {nick_collision(From, Nick, StateData),
mod_muc:can_use_nick(StateData#state.server_host,
@ -3670,54 +3727,53 @@ process_iq_mucsub(From,
ErrText = <<"That nickname is registered by another person">>,
{error, xmpp:err_conflict(ErrText, Lang)};
_ ->
NewStateData = add_online_user(
From, Nick, Role, true, Nodes, StateData),
NewStateData = set_subscriber(From, Nick, Nodes, StateData),
{result, subscribe_result(Packet), NewStateData}
end;
{ok, #user{role = Role}} ->
{ok, #subscriber{}} ->
Nodes = get_subscription_nodes(Packet),
NewStateData = add_online_user(
From, Nick, Role, true, Nodes, StateData),
NewStateData = set_subscriber(From, Nick, Nodes, StateData),
{result, subscribe_result(Packet), NewStateData};
error ->
add_new_user(From, Nick, Packet, StateData)
end;
process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]},
StateData) ->
LJID = jid:tolower(From),
case ?DICT:find(LJID, StateData#state.users) of
{ok, #user{is_subscriber = true} = User} ->
NewStateData = remove_subscription(From, User, StateData),
LBareJID = jid:tolower(jid:remove_resource(From)),
case ?DICT:find(LBareJID, StateData#state.subscribers) of
{ok, #subscriber{nick = Nick}} ->
Nicks = ?DICT:erase(Nick, StateData#state.subscriber_nicks),
Subscribers = ?DICT:erase(LBareJID, StateData#state.subscribers),
NewStateData = StateData#state{subscribers = Subscribers,
subscriber_nicks = Nicks},
store_room(NewStateData),
{result, undefined, NewStateData};
_ ->
{result, undefined, StateData}
end;
process_iq_mucsub(From, #iq{type = get, lang = Lang,
sub_els = [#muc_subscriptions{}]},
StateData) ->
FAffiliation = get_affiliation(From, StateData),
FRole = get_role(From, StateData),
if FRole == moderator; FAffiliation == owner; FAffiliation == admin ->
JIDs = dict:fold(
fun(_, #subscriber{jid = J}, Acc) ->
[J|Acc]
end, [], StateData#state.subscribers),
{result, #muc_subscriptions{list = JIDs}, StateData};
true ->
Txt = <<"Moderator privileges required">>,
{error, xmpp:err_forbidden(Txt, Lang)}
end;
process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) ->
Txt = <<"Value 'get' of 'type' attribute is not allowed">>,
{error, xmpp:err_bad_request(Txt, Lang)}.
-spec remove_subscription(jid(), #user{}, state()) -> state().
remove_subscription(JID, #user{is_subscriber = true} = User, StateData) ->
case User#user.last_presence of
undefined ->
remove_online_user(JID, StateData, false);
_ ->
LJID = jid:tolower(JID),
Users = ?DICT:store(LJID, User#user{is_subscriber = false},
StateData#state.users),
StateData#state{users = Users}
end;
remove_subscription(_JID, #user{}, StateData) ->
StateData.
-spec remove_subscriptions(state()) -> state().
remove_subscriptions(StateData) ->
if not (StateData#state.config)#config.allow_subscription ->
dict:fold(
fun(_LJID, User, State) ->
remove_subscription(User#user.jid, User, State)
end, StateData, StateData#state.users);
StateData#state{subscribers = ?DICT:new(),
subscriber_nicks = ?DICT:new()};
true ->
StateData
end.
@ -3957,18 +4013,26 @@ store_room(StateData) ->
-spec send_wrapped(jid(), jid(), stanza(), binary(), state()) -> ok.
send_wrapped(From, To, Packet, Node, State) ->
LTo = jid:tolower(To),
case ?DICT:find(LTo, State#state.users) of
{ok, #user{is_subscriber = true,
subscriptions = Nodes,
last_presence = undefined}} ->
LBareTo = jid:tolower(jid:remove_resource(To)),
IsOffline = case ?DICT:find(LTo, State#state.users) of
{ok, #user{last_presence = undefined}} -> true;
error -> true;
_ -> false
end,
if IsOffline ->
case ?DICT:find(LBareTo, State#state.subscribers) of
{ok, #subscriber{nodes = Nodes, jid = JID}} ->
case lists:member(Node, Nodes) of
true ->
NewPacket = wrap(From, To, Packet, Node),
ejabberd_router:route(State#state.jid, To, NewPacket);
NewPacket = wrap(From, JID, Packet, Node),
ejabberd_router:route(State#state.jid, JID, NewPacket);
false ->
ok
end;
_ ->
ok
end;
true ->
ejabberd_router:route(From, To, Packet)
end.
@ -3983,13 +4047,10 @@ wrap(From, To, Packet, Node) ->
id = randoms:get_string(),
xml_els = [El]}]}}]}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Multicast
-spec send_multiple(jid(), binary(), [#user{}], stanza()) -> ok.
send_multiple(From, Server, Users, Packet) ->
JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)],
ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet).
%% -spec send_multiple(jid(), binary(), [#user{}], stanza()) -> ok.
%% send_multiple(From, Server, Users, Packet) ->
%% JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)],
%% ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet).
-spec send_wrapped_multiple(jid(), [#user{}], stanza(), binary(), state()) -> ok.
send_wrapped_multiple(From, Users, Packet, Node, State) ->

View File

@ -450,12 +450,12 @@ need_to_store(LServer, #message{type = Type} = Packet) ->
(unless_chat_state) -> unless_chat_state
end,
unless_chat_state) of
true ->
true;
false ->
Packet#message.body /= [];
unless_chat_state ->
not xmpp_util:is_standalone_chat_state(Packet);
true ->
true
not xmpp_util:is_standalone_chat_state(Packet)
end
end;
true ->
@ -469,14 +469,20 @@ store_packet(From, To, Packet) ->
case check_event(From, To, Packet) of
true ->
#jid{luser = LUser, lserver = LServer} = To,
case ejabberd_hooks:run_fold(store_offline_message, LServer,
Packet, [From, To]) of
drop ->
ok;
NewPacket ->
TimeStamp = p1_time_compat:timestamp(),
Expire = find_x_expire(TimeStamp, Packet),
El = xmpp:encode(Packet),
Expire = find_x_expire(TimeStamp, NewPacket),
El = xmpp:encode(NewPacket),
gen_mod:get_module_proc(To#jid.lserver, ?PROCNAME) !
#offline_msg{us = {LUser, LServer},
timestamp = TimeStamp, expire = Expire,
from = From, to = To, packet = El},
stop;
stop
end;
_ -> ok
end;
false -> ok

View File

@ -81,7 +81,7 @@ remove_old_messages(Days, LServer) ->
[<<"DELETE FROM spool"
" WHERE created_at < "
"NOW() - INTERVAL '">>,
integer_to_list(Days), <<"';">>]) of
integer_to_list(Days), <<"' DAY;">>]) of
{updated, N} ->
?INFO_MSG("~p message(s) deleted from offline spool", [N]);
_Error ->

View File

@ -233,7 +233,7 @@ export(Server) ->
"values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s,"
" %(Order)d, %(MatchAll)b, %(MatchIQ)b,"
" %(MatchMessage)b, %(MatchPresenceIn)b,"
" %(MatchPresenceOut)b)")
" %(MatchPresenceOut)b);")
|| {SType, SValue, SAction, Order,
MatchAll, MatchIQ,
MatchMessage, MatchPresenceIn,

348
src/mod_privilege.erl Normal file
View File

@ -0,0 +1,348 @@
%%%-------------------------------------------------------------------
%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
%%% @copyright (C) 2016, Evgeny Khramtsov
%%% @doc
%%%
%%% @end
%%% Created : 11 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
%%%-------------------------------------------------------------------
-module(mod_privilege).
-behaviour(gen_server).
-behaviour(gen_mod).
%% API
-export([start_link/2]).
-export([start/2, stop/1, mod_opt_type/1, depends/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([component_connected/1, component_disconnected/2,
roster_access/2, process_message/3,
process_presence_out/4, process_presence_in/5]).
-include("ejabberd.hrl").
-include("logger.hrl").
-include("xmpp.hrl").
-record(state, {server_host = <<"">> :: binary(),
permissions = dict:new() :: ?TDICT}).
%%%===================================================================
%%% API
%%%===================================================================
start_link(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?MODULE),
gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?MODULE),
PingSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
transient, 2000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, PingSpec).
stop(Host) ->
Proc = gen_mod:get_module_proc(Host, ?MODULE),
gen_server:call(Proc, stop),
supervisor:delete_child(ejabberd_sup, Proc).
mod_opt_type(roster) -> v_roster();
mod_opt_type(message) -> v_message();
mod_opt_type(presence) -> v_presence();
mod_opt_type(_) ->
[roster, message, presence].
depends(_, _) ->
[].
-spec component_connected(binary()) -> ok.
component_connected(Host) ->
lists:foreach(
fun(ServerHost) ->
Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
gen_server:cast(Proc, {component_connected, Host})
end, ?MYHOSTS).
-spec component_disconnected(binary(), binary()) -> ok.
component_disconnected(Host, _Reason) ->
lists:foreach(
fun(ServerHost) ->
Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
gen_server:cast(Proc, {component_disconnected, Host})
end, ?MYHOSTS).
-spec process_message(jid(), jid(), stanza()) -> stop | ok.
process_message(#jid{luser = <<"">>, lresource = <<"">>} = From,
#jid{lresource = <<"">>} = To,
#message{lang = Lang, type = T} = Msg) when T /= error ->
Host = From#jid.lserver,
ServerHost = To#jid.lserver,
Permissions = get_permissions(ServerHost),
case dict:find(Host, Permissions) of
{ok, Access} ->
case proplists:get_value(message, Access, none) of
outgoing ->
forward_message(From, To, Msg);
none ->
Txt = <<"Insufficient privilege">>,
Err = xmpp:err_forbidden(Txt, Lang),
ejabberd_router:route_error(To, From, Msg, Err)
end,
stop;
error ->
%% Component is disconnected
ok
end;
process_message(_From, _To, _Stanza) ->
ok.
-spec roster_access(boolean(), iq()) -> boolean().
roster_access(true, _) ->
true;
roster_access(false, #iq{from = From, to = To, type = Type}) ->
Host = From#jid.lserver,
ServerHost = To#jid.lserver,
Permissions = get_permissions(ServerHost),
case dict:find(Host, Permissions) of
{ok, Access} ->
Permission = proplists:get_value(roster, Access, none),
(Permission == both)
orelse (Permission == get andalso Type == get)
orelse (Permission == set andalso Type == set);
error ->
%% Component is disconnected
false
end.
-spec process_presence_out(stanza(), ejabberd_c2s:state(), jid(), jid()) -> stanza().
process_presence_out(#presence{type = Type} = Pres, _C2SState,
#jid{luser = LUser, lserver = LServer} = From,
#jid{luser = LUser, lserver = LServer, lresource = <<"">>})
when Type == available; Type == unavailable ->
%% Self-presence processing
Permissions = get_permissions(LServer),
lists:foreach(
fun({Host, Access}) ->
Permission = proplists:get_value(presence, Access, none),
if Permission == roster; Permission == managed_entity ->
To = jid:make(Host),
ejabberd_router:route(
From, To, xmpp:set_from_to(Pres, From, To));
true ->
ok
end
end, dict:to_list(Permissions)),
Pres;
process_presence_out(Acc, _, _, _) ->
Acc.
-spec process_presence_in(stanza(), ejabberd_c2s:state(),
jid(), jid(), jid()) -> stanza().
process_presence_in(#presence{type = Type} = Pres, _C2SState, _,
#jid{luser = U, lserver = S} = From,
#jid{luser = LUser, lserver = LServer})
when {U, S} /= {LUser, LServer} andalso
(Type == available orelse Type == unavailable) ->
Permissions = get_permissions(LServer),
lists:foreach(
fun({Host, Access}) ->
case proplists:get_value(presence, Access, none) of
roster ->
Permission = proplists:get_value(roster, Access, none),
if Permission == both; Permission == get ->
To = jid:make(Host),
ejabberd_router:route(
From, To, xmpp:set_from_to(Pres, From, To));
true ->
ok
end;
true ->
ok
end
end, dict:to_list(Permissions)),
Pres;
process_presence_in(Acc, _, _, _, _) ->
Acc.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([Host, _Opts]) ->
ejabberd_hooks:add(component_connected, ?MODULE,
component_connected, 50),
ejabberd_hooks:add(component_disconnected, ?MODULE,
component_disconnected, 50),
ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE,
process_message, 50),
ejabberd_hooks:add(roster_remote_access, Host, ?MODULE,
roster_access, 50),
ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
process_presence_out, 50),
ejabberd_hooks:add(user_receive_packet, Host, ?MODULE,
process_presence_in, 50),
{ok, #state{server_host = Host}}.
handle_call(get_permissions, _From, State) ->
{reply, {ok, State#state.permissions}, State};
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
handle_cast({component_connected, Host}, State) ->
ServerHost = State#state.server_host,
From = jid:make(ServerHost),
To = jid:make(Host),
RosterPerm = get_roster_permission(ServerHost, Host),
PresencePerm = get_presence_permission(ServerHost, Host),
MessagePerm = get_message_permission(ServerHost, Host),
if RosterPerm /= none, PresencePerm /= none, MessagePerm /= none ->
Priv = #privilege{perms = [#privilege_perm{access = message,
type = MessagePerm},
#privilege_perm{access = roster,
type = RosterPerm},
#privilege_perm{access = presence,
type = PresencePerm}]},
?INFO_MSG("Granting permissions to external "
"component '~s': roster = ~s, presence = ~s, "
"message = ~s",
[Host, RosterPerm, PresencePerm, MessagePerm]),
Msg = #message{from = From, to = To, sub_els = [Priv]},
ejabberd_router:route(From, To, Msg),
Permissions = dict:store(Host, [{roster, RosterPerm},
{presence, PresencePerm},
{message, MessagePerm}],
State#state.permissions),
{noreply, State#state{permissions = Permissions}};
true ->
?INFO_MSG("Granting no permissions to external component '~s'",
[Host]),
{noreply, State}
end;
handle_cast({component_disconnected, Host}, State) ->
Permissions = dict:erase(Host, State#state.permissions),
{noreply, State#state{permissions = Permissions}};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, State) ->
%% Note: we don't remove component_* hooks because they are global
%% and might be registered within a module on another virtual host
Host = State#state.server_host,
ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE,
process_message, 50),
ejabberd_hooks:delete(roster_remote_access, Host, ?MODULE,
roster_access, 50),
ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
process_presence_out, 50),
ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE,
process_presence_in, 50).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
get_permissions(ServerHost) ->
Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
try gen_server:call(Proc, get_permissions) of
{ok, Permissions} ->
Permissions
catch exit:{noproc, _} ->
%% No module is loaded for this virtual host
dict:new()
end.
forward_message(From, To, Msg) ->
Host = From#jid.lserver,
ServerHost = To#jid.lserver,
case xmpp:get_subtag(Msg, #privilege{}) of
#privilege{forwarded = #forwarded{sub_els = [#message{} = SubEl]}} ->
case SubEl#message.from of
#jid{lresource = <<"">>, lserver = ServerHost} ->
ejabberd_router:route(
xmpp:get_from(SubEl), xmpp:get_to(SubEl), SubEl);
_ ->
Lang = xmpp:get_lang(Msg),
Txt = <<"Invalid 'from' attribute">>,
Err = xmpp:err_forbidden(Txt, Lang),
ejabberd_router:route_error(To, From, Msg, Err)
end;
_ ->
?ERROR_MSG("got invalid forwarded payload from external "
"component '~s':~n~s", [Host, xmpp:pp(Msg)]),
Lang = xmpp:get_lang(Msg),
Txt = <<"Invalid forwarded payload">>,
Err = xmpp:err_bad_request(Txt, Lang),
ejabberd_router:route_error(To, From, Msg, Err)
end.
get_roster_permission(ServerHost, Host) ->
Perms = gen_mod:get_module_opt(ServerHost, ?MODULE, roster,
v_roster(), []),
case match_rule(ServerHost, Host, Perms, both) of
allow ->
both;
deny ->
Get = match_rule(ServerHost, Host, Perms, get),
Set = match_rule(ServerHost, Host, Perms, set),
if Get == allow, Set == allow -> both;
Get == allow -> get;
Set == allow -> set;
true -> none
end
end.
get_message_permission(ServerHost, Host) ->
Perms = gen_mod:get_module_opt(ServerHost, ?MODULE, message,
v_message(), []),
case match_rule(ServerHost, Host, Perms, outgoing) of
allow -> outgoing;
deny -> none
end.
get_presence_permission(ServerHost, Host) ->
Perms = gen_mod:get_module_opt(ServerHost, ?MODULE, presence,
v_presence(), []),
case match_rule(ServerHost, Host, Perms, roster) of
allow ->
roster;
deny ->
case match_rule(ServerHost, Host, Perms, managed_entity) of
allow -> managed_entity;
deny -> none
end
end.
match_rule(ServerHost, Host, Perms, Type) ->
Access = proplists:get_value(Type, Perms, none),
acl:match_rule(ServerHost, Access, jid:make(Host)).
v_roster() ->
fun(Props) ->
lists:map(
fun({both, ACL}) -> {both, acl:access_rules_validator(ACL)};
({get, ACL}) -> {get, acl:access_rules_validator(ACL)};
({set, ACL}) -> {set, acl:access_rules_validator(ACL)}
end, Props)
end.
v_message() ->
fun(Props) ->
lists:map(
fun({outgoing, ACL}) -> {outgoing, acl:access_rules_validator(ACL)}
end, Props)
end.
v_presence() ->
fun(Props) ->
lists:map(
fun({managed_entity, ACL}) ->
{managed_entity, acl:access_rules_validator(ACL)};
({roster, ACL}) ->
{roster, acl:access_rules_validator(ACL)}
end, Props)
end.

View File

@ -385,8 +385,6 @@ depends(ServerHost, Opts) ->
%% The default plugin module is implicit.
%% <p>The Erlang code for the plugin is located in a module called
%% <em>node_plugin</em>. The 'node_' prefix is mandatory.</p>
%% <p>The modules are initialized in alphetical order and the list is checked
%% and sorted to ensure that each module is initialized only once.</p>
%% <p>See {@link node_hometree:init/1} for an example implementation.</p>
init_plugins(Host, ServerHost, Opts) ->
TreePlugin = tree(Host, gen_mod:get_opt(nodetree, Opts,

View File

@ -139,15 +139,18 @@ stop(Host) ->
depends(_Host, _Opts) ->
[].
process_iq(#iq{from = #jid{luser = <<"">>},
to = #jid{resource = <<"">>}} = IQ) ->
process_iq_manager(IQ);
process_iq(#iq{from = #jid{luser = U, lserver = S},
to = #jid{luser = U, lserver = S}} = IQ) ->
process_local_iq(IQ);
process_iq(#iq{lang = Lang} = IQ) ->
process_iq(#iq{lang = Lang, to = To} = IQ) ->
case ejabberd_hooks:run_fold(roster_remote_access,
To#jid.lserver, false, [IQ]) of
false ->
Txt = <<"Query to another users is forbidden">>,
xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)).
xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang));
true ->
process_local_iq(IQ)
end.
process_local_iq(#iq{type = set,lang = Lang,
sub_els = [#roster_query{
@ -251,10 +254,10 @@ write_roster_version(LUser, LServer, InTransaction) ->
%% - roster versioning is not used by the client OR
%% - roster versioning is used by server and client, BUT the server isn't storing versions on db OR
%% - the roster version from client don't match current version.
process_iq_get(#iq{from = From, to = To, lang = Lang,
process_iq_get(#iq{to = To, lang = Lang,
sub_els = [#roster_query{ver = RequestedVersion}]} = IQ) ->
LUser = From#jid.luser,
LServer = From#jid.lserver,
LUser = To#jid.luser,
LServer = To#jid.lserver,
US = {LUser, LServer},
try {ItemsToSend, VersionToSend} =
case {roster_versioning_enabled(LServer),
@ -303,7 +306,7 @@ process_iq_get(#iq{from = From, to = To, lang = Lang,
end)
catch E:R ->
?ERROR_MSG("failed to process roster get for ~s: ~p",
[jid:to_string(From), {E, {R, erlang:get_stacktrace()}}]),
[jid:to_string(To), {E, {R, erlang:get_stacktrace()}}]),
Txt = <<"Roster module has failed">>,
xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
end.
@ -369,10 +372,10 @@ get_roster_by_jid_t(LUser, LServer, LJID) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:get_roster_by_jid(LUser, LServer, LJID).
process_iq_set(#iq{from = From, to = To, id = Id,
process_iq_set(#iq{from = From, to = To,
sub_els = [#roster_query{items = QueryItems}]} = IQ) ->
Managed = is_managed_from_id(Id),
#jid{user = User, luser = LUser, lserver = LServer} = From,
#jid{user = User, luser = LUser, lserver = LServer} = To,
Managed = {From#jid.luser, From#jid.lserver} /= {LUser, LServer},
F = fun () ->
lists:map(
fun(#roster_item{jid = JID1} = QueryItem) ->
@ -397,11 +400,10 @@ process_iq_set(#iq{from = From, to = To, id = Id,
{atomic, ItemPairs} ->
lists:foreach(
fun({OldItem, Item}) ->
send_itemset_to_managers(From, Item, Managed),
push_item(User, LServer, To, Item),
case Item#roster.subscription of
remove ->
send_unsubscribing_presence(From, OldItem);
send_unsubscribing_presence(To, OldItem);
_ ->
ok
end
@ -1012,66 +1014,6 @@ webadmin_user(Acc, _User, _Server, Lang) ->
[?XE(<<"h3">>, [?ACT(<<"roster/">>, <<"Roster">>)])].
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Implement XEP-0321 Remote Roster Management
process_iq_manager(#iq{from = From, to = To, lang = Lang} = IQ) ->
%% Check what access is allowed for From to To
MatchDomain = From#jid.lserver,
case is_domain_managed(MatchDomain, To#jid.lserver) of
true ->
process_iq_manager2(MatchDomain, IQ);
false ->
Txt = <<"Roster management is not allowed from this domain">>,
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang))
end.
process_iq_manager2(MatchDomain, #iq{to = To} = IQ) ->
%% If IQ is SET, filter the input IQ
IQFiltered = maybe_filter_request(MatchDomain, IQ),
%% Call the standard function with reversed JIDs
IdInitial = IQFiltered#iq.id,
ResIQ = process_iq(IQFiltered#iq{from = To, to = To,
id = <<"roster-remotely-managed">>}),
%% Filter the output IQ
filter_stanza(MatchDomain, ResIQ#iq{id = IdInitial}).
is_domain_managed(ContactHost, UserHost) ->
Managers = gen_mod:get_module_opt(UserHost, ?MODULE, managers,
fun(B) when is_list(B) -> B end,
[]),
lists:member(ContactHost, Managers).
maybe_filter_request(MatchDomain, IQ) when IQ#iq.type == set ->
filter_stanza(MatchDomain, IQ);
maybe_filter_request(_MatchDomain, IQ) ->
IQ.
filter_stanza(MatchDomain,
#iq{sub_els = [#roster_query{items = Items} = R]} = IQ) ->
ItemsFiltered = lists:filter(
fun(#roster_item{jid = #jid{lserver = S}}) ->
S == MatchDomain
end, Items),
IQ#iq{sub_els = [R#roster_query{items = ItemsFiltered}]}.
send_itemset_to_managers(_From, _Item, true) ->
ok;
send_itemset_to_managers(From, Item, false) ->
{_, UserHost} = Item#roster.us,
{_ContactUser, ContactHost, _ContactResource} = Item#roster.jid,
%% Check if the component is an allowed manager
IsManager = is_domain_managed(ContactHost, UserHost),
case IsManager of
true -> push_item(<<"">>, ContactHost, <<"">>, From, Item);
false -> ok
end.
is_managed_from_id(<<"roster-remotely-managed">>) ->
true;
is_managed_from_id(_Id) ->
false.
has_duplicated_groups(Groups) ->
GroupsPrep = lists:usort([jid:resourceprep(G) || G <- Groups]),
not (length(GroupsPrep) == length(Groups)).

View File

@ -127,13 +127,8 @@ get_local_stat(Server, [], Name)
end;
get_local_stat(_Server, [], Name)
when Name == <<"users/all-hosts/online">> ->
case catch mnesia:table_info(session, size) of
{'EXIT', _Reason} ->
?STATERR(500, <<"Internal Server Error">>);
Users ->
?STATVAL((iolist_to_binary(integer_to_list(Users))),
<<"users">>)
end;
Users = ejabberd_sm:connected_users_number(),
?STATVAL((iolist_to_binary(integer_to_list(Users))), <<"users">>);
get_local_stat(_Server, [], Name)
when Name == <<"users/all-hosts/total">> ->
NumUsers = lists:foldl(fun (Host, Total) ->

View File

@ -664,7 +664,7 @@ get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex,
Before /= undefined -> {<<">">>, <<"asc">>};
true -> {<<"is not">>, <<"desc">>}
end,
SNidx = integer_to_binary(Nidx),
SNidx = jlib:i2l(Nidx),
I = if After /= undefined -> After;
Before /= undefined -> Before;
true -> undefined
@ -774,7 +774,7 @@ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM
get_last_items(Nidx, _From, Count) ->
Limit = jlib:i2l(Count),
SNidx = integer_to_binary(Nidx),
SNidx = jlib:i2l(Nidx),
Query = fun(mssql, _) ->
ejabberd_sql:sql_query_t(
[<<"select top ">>, Limit,
@ -876,7 +876,7 @@ del_items(Nidx, [ItemId]) ->
del_item(Nidx, ItemId);
del_items(Nidx, ItemIds) ->
I = str:join([[<<"'">>, ejabberd_sql:escape(X), <<"'">>] || X <- ItemIds], <<",">>),
SNidx = integer_to_binary(Nidx),
SNidx = jlib:i2l(Nidx),
catch
ejabberd_sql:sql_query_t([<<"delete from pubsub_item where itemid in (">>,
I, <<") and nodeid='">>, SNidx, <<"';">>]).
@ -932,8 +932,9 @@ select_affiliation_subscriptions(Nidx, JID, JID) ->
select_affiliation_subscriptions(Nidx, JID);
select_affiliation_subscriptions(Nidx, GenKey, SubKey) ->
{result, Affiliation} = get_affiliation(Nidx, GenKey),
{result, Subscriptions} = get_subscriptions(Nidx, SubKey),
{Affiliation, Subscriptions}.
{result, BareJidSubs} = get_subscriptions(Nidx, GenKey),
{result, FullJidSubs} = get_subscriptions(Nidx, SubKey),
{Affiliation, BareJidSubs++FullJidSubs}.
update_affiliation(Nidx, JID, Affiliation) ->
J = encode_jid(JID),

View File

@ -37,6 +37,7 @@
%%% plugins:
%%% - "flat"
%%% - "pep" # Requires mod_caps.
%%% - "mb"
%%% pep_mapping:
%%% "urn:xmpp:microblog:0": "mb"
%%% </pre></p>
@ -153,7 +154,7 @@ set_subscriptions(Nidx, Owner, Subscription, SubId) ->
node_pep:set_subscriptions(Nidx, Owner, Subscription, SubId).
get_pending_nodes(Host, Owner) ->
node_hometree:get_pending_nodes(Host, Owner).
node_pep:get_pending_nodes(Host, Owner).
get_states(Nidx) ->
node_pep:get_states(Nidx).

Some files were not shown because too many files have changed in this diff Show More