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:
commit
78a44e0176
@ -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
169
config/ejabberd.exs
Normal 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
667
config/ejabberd.yml
Normal 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
|
@ -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
|
||||
|
@ -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: {}
|
||||
|
@ -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{
|
||||
|
26
include/ejabberd_oauth.hrl
Normal file
26
include/ejabberd_oauth.hrl
Normal 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'
|
||||
}).
|
20
include/ejabberd_service.hrl
Normal file
20
include/ejabberd_service.hrl
Normal 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{} ).
|
@ -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()}
|
||||
|
@ -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,
|
||||
|
@ -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">>).
|
||||
|
@ -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().
|
||||
|
@ -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
119
lib/ejabberd/config/attr.ex
Normal 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
|
145
lib/ejabberd/config/config.ex
Normal file
145
lib/ejabberd/config/config.ex
Normal 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
|
23
lib/ejabberd/config/ejabberd_hook.ex
Normal file
23
lib/ejabberd/config/ejabberd_hook.ex
Normal 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
|
70
lib/ejabberd/config/ejabberd_module.ex
Normal file
70
lib/ejabberd/config/ejabberd_module.ex
Normal 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
|
32
lib/ejabberd/config/logger/ejabberd_logger.ex
Normal file
32
lib/ejabberd/config/logger/ejabberd_logger.ex
Normal 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
|
46
lib/ejabberd/config/opts_formatter.ex
Normal file
46
lib/ejabberd/config/opts_formatter.ex
Normal 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
|
55
lib/ejabberd/config/store.ex
Normal file
55
lib/ejabberd/config/store.ex
Normal 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
|
40
lib/ejabberd/config/validator/validation.ex
Normal file
40
lib/ejabberd/config/validator/validation.ex
Normal 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
|
28
lib/ejabberd/config/validator/validator_attrs.ex
Normal file
28
lib/ejabberd/config/validator/validator_attrs.ex
Normal 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
|
30
lib/ejabberd/config/validator/validator_dependencies.ex
Normal file
30
lib/ejabberd/config/validator/validator_dependencies.ex
Normal 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
|
30
lib/ejabberd/config/validator/validator_utility.ex
Normal file
30
lib/ejabberd/config/validator/validator_utility.ex
Normal 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
|
18
lib/ejabberd/config_util.ex
Normal file
18
lib/ejabberd/config_util.ex
Normal 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
19
lib/ejabberd/module.ex
Normal 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
|
94
lib/mix/tasks/deps.tree.ex
Normal file
94
lib/mix/tasks/deps.tree.ex
Normal 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
|
@ -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
69
mix.exs
@ -3,7 +3,7 @@ defmodule Ejabberd.Mixfile do
|
||||
|
||||
def project do
|
||||
[app: :ejabberd,
|
||||
version: "16.06.0",
|
||||
version: "16.11.0",
|
||||
description: description,
|
||||
elixir: "~> 1.2",
|
||||
elixirc_paths: ["lib"],
|
||||
@ -11,11 +11,13 @@ defmodule Ejabberd.Mixfile do
|
||||
compilers: [:asn1] ++ Mix.compilers,
|
||||
erlc_options: erlc_options,
|
||||
erlc_paths: ["asn1", "src"],
|
||||
# Elixir tests are starting the part of ejabberd they need
|
||||
aliases: [test: "test --no-start"],
|
||||
package: package,
|
||||
deps: deps]
|
||||
end
|
||||
|
||||
defp description do
|
||||
def description do
|
||||
"""
|
||||
Robust, ubiquitous and massively scalable Jabber / XMPP Instant Messaging platform.
|
||||
"""
|
||||
@ -26,9 +28,8 @@ defmodule Ejabberd.Mixfile do
|
||||
applications: [:ssl],
|
||||
included_applications: [:lager, :mnesia, :p1_utils, :cache_tab,
|
||||
:fast_tls, :stringprep, :fast_xml,
|
||||
:stun, :fast_yaml, :ezlib, :iconv,
|
||||
:esip, :jiffy, :p1_oauth2, :p1_xmlrpc, :eredis,
|
||||
:p1_mysql, :p1_pgsql, :sqlite3]]
|
||||
:stun, :fast_yaml, :esip, :jiffy, :p1_oauth2]
|
||||
++ cond_apps]
|
||||
end
|
||||
|
||||
defp erlc_options do
|
||||
@ -38,7 +39,7 @@ defmodule Ejabberd.Mixfile do
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[{:lager, "~> 3.0.0"},
|
||||
[{:lager, "~> 3.2"},
|
||||
{:p1_utils, "~> 1.0"},
|
||||
{:cache_tab, "~> 1.0"},
|
||||
{:stringprep, "~> 1.0"},
|
||||
@ -49,17 +50,40 @@ defmodule Ejabberd.Mixfile do
|
||||
{:esip, "~> 1.0"},
|
||||
{:jiffy, "~> 0.14.7"},
|
||||
{:p1_oauth2, "~> 0.6.1"},
|
||||
{:p1_xmlrpc, "~> 1.15"},
|
||||
{:p1_mysql, "~> 1.0"},
|
||||
{:p1_pgsql, "~> 1.1"},
|
||||
{:sqlite3, "~> 1.1"},
|
||||
{:ezlib, "~> 1.0"},
|
||||
{:iconv, "~> 1.0"},
|
||||
{:eredis, "~> 1.0"},
|
||||
{:exrm, "~> 1.0.0-rc7", only: :dev}]
|
||||
{:exrm, "~> 1.0.0", only: :dev},
|
||||
# relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with
|
||||
# version 3.20:
|
||||
{:relx, "~> 3.21", only: :dev},
|
||||
{:ex_doc, ">= 0.0.0", only: :dev}]
|
||||
++ cond_deps
|
||||
end
|
||||
|
||||
defp package do
|
||||
defp cond_deps do
|
||||
for {:true, dep} <- [{config(:mysql), {:p1_mysql, "~> 1.0"}},
|
||||
{config(:pgsql), {:p1_pgsql, "~> 1.1"}},
|
||||
{config(:sqlite), {:sqlite3, "~> 1.1"}},
|
||||
{config(:riak), {:riakc, "~> 2.4"}},
|
||||
{config(:redis), {:eredis, "~> 1.0"}},
|
||||
{config(:zlib), {:ezlib, "~> 1.0"}},
|
||||
{config(:iconv), {:iconv, "~> 1.0"}},
|
||||
{config(:pam), {:p1_pam, "~> 1.0"}},
|
||||
{config(:tools), {:luerl, github: "rvirding/luerl", tag: "v0.2"}},
|
||||
{config(:tools), {:meck, "~> 0.8.4"}},
|
||||
{config(:tools), {:moka, github: "processone/moka", tag: "1.0.5c"}}], do:
|
||||
dep
|
||||
end
|
||||
|
||||
defp cond_apps do
|
||||
for {:true, app} <- [{config(:redis), :eredis},
|
||||
{config(:mysql), :p1_mysql},
|
||||
{config(:pgsql), :p1_pgsql},
|
||||
{config(:sqlite), :sqlite3},
|
||||
{config(:zlib), :ezlib},
|
||||
{config(:iconv), :iconv}], do:
|
||||
app
|
||||
end
|
||||
|
||||
def package do
|
||||
[# These are the default files included in the package
|
||||
files: ["lib", "src", "priv", "mix.exs", "include", "README.md", "COPYING"],
|
||||
maintainers: ["ProcessOne"],
|
||||
@ -69,6 +93,21 @@ defmodule Ejabberd.Mixfile do
|
||||
"Source" => "https://github.com/processone/ejabberd",
|
||||
"ProcessOne" => "http://www.process-one.net/"}]
|
||||
end
|
||||
|
||||
def vars do
|
||||
case :file.consult("vars.config") do
|
||||
{:ok,config} -> config
|
||||
_ -> [zlib: true, iconv: true]
|
||||
end
|
||||
end
|
||||
|
||||
defp config(key) do
|
||||
case vars[key] do
|
||||
nil -> false
|
||||
value -> value
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
defmodule Mix.Tasks.Compile.Asn1 do
|
||||
|
33
mix.lock
33
mix.lock
@ -1,26 +1,23 @@
|
||||
%{"bbmustache": {:hex, :bbmustache, "1.0.4", "7ba94f971c5afd7b6617918a4bb74705e36cab36eb84b19b6a1b7ee06427aa38", [:rebar], []},
|
||||
"cache_tab": {:hex, :cache_tab, "1.0.3", "0e3c40dde2fe2a6a4db241d7583cea0cc1bcf29e546a0a22f15b75366b2f336e", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
|
||||
"cache_tab": {:hex, :cache_tab, "1.0.4", "3fd2b1ab40c36e7830a4e09e836c6b0fa89191cd4e5fd471873e4eb42f5cd37c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
|
||||
"cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []},
|
||||
"eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []},
|
||||
"earmark": {:hex, :earmark, "1.0.2", "a0b0904d74ecc14da8bd2e6e0248e1a409a2bc91aade75fcf428125603de3853", [:mix], []},
|
||||
"erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]},
|
||||
"esip": {:hex, :esip, "1.0.6", "cb1ced88fae4c4a4888d9023c2c13b2239e14f8e360aee134c964b4a36dcc34d", [:rebar3], [{:stun, "1.0.5", [hex: :stun, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.5", [hex: :fast_tls, optional: false]}]},
|
||||
"exrm": {:hex, :exrm, "1.0.6", "f708fc091dcacb93c1da58254a1ab34166d5ac3dca162877e878fe5d7a9e9dce", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]},
|
||||
"esip": {:hex, :esip, "1.0.8", "69885a6c07964aabc6c077fe1372aa810a848bd3d9a415b160dabdce9c7a79b5", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}, {:stun, "1.0.7", [hex: :stun, optional: false]}]},
|
||||
"ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]},
|
||||
"exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]},
|
||||
"ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []},
|
||||
"fast_tls": {:hex, :fast_tls, "1.0.5", "8b970a91d4131fe5b9d47ffaccc2466944293c88dc5cc75a25548d73d57f7b77", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
|
||||
"fast_xml": {:hex, :fast_xml, "1.1.13", "85eca0a003598dbb0644320bd9bdc5fef30ad6285ab2aa80e2b5b82e65b79aa8", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
|
||||
"fast_yaml": {:hex, :fast_yaml, "1.0.4", "075ffb55f6ff3aa2f0461b8bfd1218e2f91e632c1675fc535963b9de7834800e", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
|
||||
"fast_tls": {:hex, :fast_tls, "1.0.7", "9b72ecfcdcad195ab072c196fab8334f49d8fea76bf1a51f536d69e7527d902a", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
|
||||
"fast_xml": {:hex, :fast_xml, "1.1.15", "6d23eb7f874e1357cf80a48d75a7bd0c8f6318029dc4b70122e9f54911f57f83", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
|
||||
"fast_yaml": {:hex, :fast_yaml, "1.0.6", "3fe6feb7935ae8028b337e53e1db29e73ad3bca8041108f6a8f73b7175ece75c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
|
||||
"getopt": {:hex, :getopt, "0.8.2", "b17556db683000ba50370b16c0619df1337e7af7ecbf7d64fbf8d1d6bce3109b", [:rebar], []},
|
||||
"goldrush": {:hex, :goldrush, "0.1.7", "349a351d17c71c2fdaa18a6c2697562abe136fec945f147b381f0cf313160228", [:rebar3], []},
|
||||
"iconv": {:hex, :iconv, "1.0.0", "5ff1c54e5b3b9a8235de872632e9612c7952acdf89bc21db2f2efae0e72647be", [:rebar3], []},
|
||||
"goldrush": {:hex, :goldrush, "0.1.8", "2024ba375ceea47e27ea70e14d2c483b2d8610101b4e852ef7f89163cdb6e649", [:rebar3], []},
|
||||
"iconv": {:hex, :iconv, "1.0.2", "a0792f06ab4b5ea1b5bb49789405739f1281a91c44cf3879cb70e4d777666217", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
|
||||
"jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []},
|
||||
"lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]},
|
||||
"p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []},
|
||||
"lager": {:hex, :lager, "3.2.1", "eef4e18b39e4195d37606d9088ea05bf1b745986cf8ec84f01d332456fe88d17", [:rebar3], [{:goldrush, "0.1.8", [hex: :goldrush, optional: false]}]},
|
||||
"p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []},
|
||||
"p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []},
|
||||
"p1_utils": {:hex, :p1_utils, "1.0.4", "7face65db102b5d1ebe7ad3c7517c5ee8cfbe174c6658e3affbb00eb66e06787", [:rebar3], []},
|
||||
"p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []},
|
||||
"p1_utils": {:hex, :p1_utils, "1.0.5", "3e698354fdc1fea5491d991457b0cb986c0a00a47d224feb841dc3ec82b9f721", [:rebar3], []},
|
||||
"providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]},
|
||||
"relx": {:hex, :relx, "3.20.0", "b515b8317d25b3a1508699294c3d1fa6dc0527851dffc87446661bce21a36710", [:rebar3], [{:providers, "1.6.0", [hex: :providers, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}]},
|
||||
"sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []},
|
||||
"stringprep": {:hex, :stringprep, "1.0.4", "f8f94d838ed202787699ff71d67b65481d350bda32b232ba1db52faca8eaed39", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
|
||||
"stun": {:hex, :stun, "1.0.5", "ec1d9928f25451d6fd2d2ade58c46b58b8d2c8326ddea3a667e926d04792f82c", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.5", [hex: :fast_tls, optional: false]}]}}
|
||||
"relx": {:hex, :relx, "3.21.1", "f989dc520730efd9075e9f4debcb8ba1d7d1e86b018b0bcf45a2eb80270b4ad6", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]},
|
||||
"stringprep": {:hex, :stringprep, "1.0.6", "1cf1c439eb038aa590da5456e019f86afbfbfeb5a2d37b6e5f873041624c6701", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
|
||||
"stun": {:hex, :stun, "1.0.7", "904dc6f26a3c30c54881c4c3003699f2a4968067ee6b3aecdf9895aad02df75e", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}}
|
||||
|
@ -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"}.
|
||||
|
@ -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:"
|
||||
|
23
rebar.config
23
rebar.config
@ -9,16 +9,15 @@
|
||||
|
||||
{deps, [{lager, ".*", {git, "https://github.com/basho/lager", {tag, "3.2.1"}}},
|
||||
{p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.5"}}},
|
||||
{cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.3"}}},
|
||||
{fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.6"}}},
|
||||
{stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.5"}}},
|
||||
{fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.14"}}},
|
||||
{stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.6"}}},
|
||||
{esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.7"}}},
|
||||
{fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.5"}}},
|
||||
{cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.4"}}},
|
||||
{fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.7"}}},
|
||||
{stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.6"}}},
|
||||
{fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.15"}}},
|
||||
{stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.7"}}},
|
||||
{esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.8"}}},
|
||||
{fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.6"}}},
|
||||
{jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.7"}}},
|
||||
{p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.1"}}},
|
||||
{p1_xmlrpc, ".*", {git, "https://github.com/processone/p1_xmlrpc", {tag, "1.15.1"}}},
|
||||
{luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.2"}}},
|
||||
{if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql",
|
||||
{tag, "1.0.1"}}}},
|
||||
@ -34,16 +33,16 @@
|
||||
{tag, "2.4.1"}}}},
|
||||
%% Elixir support, needed to run tests
|
||||
{if_var_true, elixir, {elixir, ".*", {git, "https://github.com/elixir-lang/elixir",
|
||||
{tag, "v1.1.1"}}}},
|
||||
{tag, {if_version_above, "17", "v1.2.6", "v1.1.1"}}}}},
|
||||
%% TODO: When modules are fully migrated to new structure and mix, we will not need anymore rebar_elixir_plugin
|
||||
{if_var_true, elixir, {rebar_elixir_plugin, ".*",
|
||||
{git, "https://github.com/processone/rebar_elixir_plugin", "0.1.0"}}},
|
||||
{if_var_true, iconv, {iconv, ".*", {git, "https://github.com/processone/iconv",
|
||||
{tag, "1.0.1"}}}},
|
||||
{tag, "1.0.2"}}}},
|
||||
{if_var_true, tools, {meck, "0.8.*", {git, "https://github.com/eproxus/meck",
|
||||
{tag, "0.8.4"}}}},
|
||||
{if_var_true, tools, {moka, ".*", {git, "https://github.com/processone/moka.git",
|
||||
{tag, "1.0.5b"}}}},
|
||||
{tag, "1.0.5c"}}}},
|
||||
{if_var_true, redis, {eredis, ".*", {git, "https://github.com/wooga/eredis",
|
||||
{tag, "v1.0.8"}}}}]}.
|
||||
|
||||
@ -70,7 +69,9 @@
|
||||
{if_var_true, debug, debug_info},
|
||||
{if_var_true, roster_gateway_workaround, {d, 'ROSTER_GATWAY_WORKAROUND'}},
|
||||
{if_var_match, db_type, mssql, {d, 'mssql'}},
|
||||
{if_var_true, elixir, {d, 'ELIXIR_ENABLED'}},
|
||||
{if_var_true, erlang_deprecated_types, {d, 'ERL_DEPRECATED_TYPES'}},
|
||||
{if_version_above, "18", {d, 'STRONG_RAND_BYTES'}},
|
||||
{if_var_true, hipe, native},
|
||||
{src_dirs, [asn1, src,
|
||||
{if_var_true, tools, tools},
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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, <<":">>),
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
60
src/acl.erl
60
src/acl.erl
@ -31,11 +31,13 @@
|
||||
|
||||
-export([add_access/3, clear/0]).
|
||||
-export([start/0, add/3, add_list/3, add_local/3, add_list_local/3,
|
||||
load_from_config/0, match_rule/3,
|
||||
load_from_config/0, match_rule/3, any_rules_allowed/3,
|
||||
transform_options/1, opt_type/1, acl_rule_matches/3,
|
||||
acl_rule_verify/1, access_matches/3,
|
||||
transform_access_rules_config/1,
|
||||
access_rules_validator/1, shaper_rules_validator/1]).
|
||||
parse_ip_netmask/1,
|
||||
access_rules_validator/1, shaper_rules_validator/1,
|
||||
normalize_spec/1, resolve_access/2]).
|
||||
|
||||
-include("ejabberd.hrl").
|
||||
-include("logger.hrl").
|
||||
@ -74,12 +76,6 @@
|
||||
-export_type([acl/0]).
|
||||
|
||||
start() ->
|
||||
case catch mnesia:table_info(acl, storage_type) of
|
||||
disc_copies ->
|
||||
mnesia:delete_table(acl);
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
mnesia:create_table(acl,
|
||||
[{ram_copies, [node()]}, {type, bag},
|
||||
{local_content, true},
|
||||
@ -261,6 +257,7 @@ normalize_spec(Spec) ->
|
||||
{server, S} -> {server, nameprep(S)};
|
||||
{resource, R} -> {resource, resourceprep(R)};
|
||||
{server_regexp, SR} -> {server_regexp, b(SR)};
|
||||
{resource_regexp, R} -> {resource_regexp, b(R)};
|
||||
{server_glob, S} -> {server_glob, b(S)};
|
||||
{resource_glob, R} -> {resource_glob, b(R)};
|
||||
{ip, {Net, Mask}} -> {ip, {Net, Mask}};
|
||||
@ -274,6 +271,15 @@ normalize_spec(Spec) ->
|
||||
end
|
||||
end.
|
||||
|
||||
-spec any_rules_allowed(global | binary(), access_name(),
|
||||
jid() | ljid() | inet:ip_address()) -> boolean().
|
||||
|
||||
any_rules_allowed(Host, Access, Entity) ->
|
||||
lists:any(fun (Rule) ->
|
||||
allow == acl:match_rule(Host, Rule, Entity)
|
||||
end,
|
||||
Access).
|
||||
|
||||
-spec match_rule(global | binary(), access_name(),
|
||||
jid() | ljid() | inet:ip_address()) -> any().
|
||||
|
||||
@ -432,30 +438,35 @@ acl_rule_matches({node_glob, {UR, SR}}, #{usr := {U, S, _}}, _Host) ->
|
||||
acl_rule_matches(_ACL, _Data, _Host) ->
|
||||
false.
|
||||
|
||||
-spec access_matches(atom()|list(), any(), global|binary()) -> any().
|
||||
access_matches(all, _Data, _Host) ->
|
||||
allow;
|
||||
access_matches(none, _Data, _Host) ->
|
||||
deny;
|
||||
access_matches(Name, Data, Host) when is_atom(Name) ->
|
||||
GAccess = ets:lookup(access, {Name, global}),
|
||||
resolve_access(all, _Host) ->
|
||||
all;
|
||||
resolve_access(none, _Host) ->
|
||||
none;
|
||||
resolve_access(Name, Host) when is_atom(Name) ->
|
||||
GAccess = mnesia:dirty_read(access, {Name, global}),
|
||||
LAccess =
|
||||
if Host /= global -> ets:lookup(access, {Name, Host});
|
||||
if Host /= global -> mnesia:dirty_read(access, {Name, Host});
|
||||
true -> []
|
||||
end,
|
||||
case GAccess ++ LAccess of
|
||||
[] ->
|
||||
deny;
|
||||
[];
|
||||
AccessList ->
|
||||
Rules = lists:flatmap(
|
||||
lists:flatmap(
|
||||
fun(#access{rules = Rs}) ->
|
||||
Rs
|
||||
end, AccessList),
|
||||
access_rules_matches(Rules, Data, Host)
|
||||
end, AccessList)
|
||||
end;
|
||||
access_matches(Rules, Data, Host) when is_list(Rules) ->
|
||||
access_rules_matches(Rules, Data, Host).
|
||||
resolve_access(Rules, _Host) when is_list(Rules) ->
|
||||
Rules.
|
||||
|
||||
-spec access_matches(atom()|list(), any(), global|binary()) -> allow|deny.
|
||||
access_matches(Rules, Data, Host) ->
|
||||
case resolve_access(Rules, Host) of
|
||||
all -> allow;
|
||||
none -> deny;
|
||||
RRules -> access_rules_matches(RRules, Data, Host)
|
||||
end.
|
||||
|
||||
-spec access_rules_matches(list(), any(), global|binary()) -> any().
|
||||
|
||||
@ -473,7 +484,7 @@ access_rules_matches([], _Data, _Host, Default) ->
|
||||
Default.
|
||||
|
||||
get_aclspecs(ACL, Host) ->
|
||||
ets:lookup(acl, {ACL, Host}) ++ ets:lookup(acl, {ACL, global}).
|
||||
mnesia:dirty_read(acl, {ACL, Host}) ++ mnesia:dirty_read(acl, {ACL, global}).
|
||||
|
||||
is_regexp_match(String, RegExp) ->
|
||||
case ejabberd_regexp:run(String, RegExp) of
|
||||
@ -676,7 +687,8 @@ transform_options({acl, Name, Type}, Opts) ->
|
||||
{server_regexp, SR} -> {server_regexp, [b(SR)]};
|
||||
{server_glob, S} -> {server_glob, [b(S)]};
|
||||
{ip, S} -> {ip, [b(S)]};
|
||||
{resource_glob, R} -> {resource_glob, [b(R)]}
|
||||
{resource_glob, R} -> {resource_glob, [b(R)]};
|
||||
{resource_regexp, R} -> {resource_regexp, [b(R)]}
|
||||
end,
|
||||
[{acl, [{Name, [T]}]}|Opts];
|
||||
transform_options({access, Name, Rules}, Opts) ->
|
||||
|
@ -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'}
|
||||
|
@ -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=",
|
||||
|
@ -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),
|
||||
|
543
src/ejabberd_access_permissions.erl
Normal file
543
src/ejabberd_access_permissions.erl
Normal 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].
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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 =
|
||||
|
@ -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 =
|
||||
|
@ -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 =
|
||||
|
@ -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},
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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) ->
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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].
|
||||
|
65
src/ejabberd_oauth_mnesia.erl
Normal file
65
src/ejabberd_oauth_mnesia.erl
Normal 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).
|
||||
|
98
src/ejabberd_oauth_rest.erl
Normal file
98
src/ejabberd_oauth_rest.erl
Normal 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].
|
78
src/ejabberd_oauth_sql.erl
Normal file
78
src/ejabberd_oauth_sql.erl
Normal 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")).
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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().
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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};
|
||||
|
@ -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=%%%%,%%%=:
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
358
src/http_p1.erl
Normal 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
|
||||
|
26
src/jid.erl
26
src/jid.erl
@ -52,11 +52,35 @@
|
||||
-spec start() -> ok.
|
||||
|
||||
start() ->
|
||||
{ok, Owner} = ets_owner(),
|
||||
SplitPattern = binary:compile_pattern([<<"@">>, <<"/">>]),
|
||||
catch ets:new(jlib, [named_table, protected, set, {keypos, 1}]),
|
||||
%% Table is public to allow ETS insert to fix / update the table even if table already exist
|
||||
%% with another owner.
|
||||
catch ets:new(jlib, [named_table, public, set, {keypos, 1}, {heir, Owner, undefined}]),
|
||||
ets:insert(jlib, {string_to_jid_pattern, SplitPattern}),
|
||||
ok.
|
||||
|
||||
ets_owner() ->
|
||||
case whereis(jlib_ets) of
|
||||
undefined ->
|
||||
Pid = spawn(fun() -> ets_keepalive() end),
|
||||
case catch register(jlib_ets, Pid) of
|
||||
true ->
|
||||
{ok, Pid};
|
||||
Error -> Error
|
||||
end;
|
||||
Pid ->
|
||||
{ok,Pid}
|
||||
end.
|
||||
|
||||
%% Process used to keep jlib ETS table alive in case the original owner dies.
|
||||
%% The table need to be public, otherwise subsequent inserts would fail.
|
||||
ets_keepalive() ->
|
||||
receive
|
||||
_ ->
|
||||
ets_keepalive()
|
||||
end.
|
||||
|
||||
-spec make(binary(), binary(), binary()) -> jid() | error.
|
||||
|
||||
make(User, Server, Resource) ->
|
||||
|
36
src/jlib.erl
36
src/jlib.erl
@ -373,15 +373,20 @@ iq_type_to_string(error) -> <<"error">>.
|
||||
-spec iq_to_xml(IQ :: iq()) -> xmlel().
|
||||
|
||||
iq_to_xml(#iq{id = ID, type = Type, sub_el = SubEl}) ->
|
||||
Children =
|
||||
if
|
||||
is_list(SubEl) -> SubEl;
|
||||
true -> [SubEl]
|
||||
end,
|
||||
if ID /= <<"">> ->
|
||||
#xmlel{name = <<"iq">>,
|
||||
attrs =
|
||||
[{<<"id">>, ID}, {<<"type">>, iq_type_to_string(Type)}],
|
||||
children = SubEl};
|
||||
children = Children};
|
||||
true ->
|
||||
#xmlel{name = <<"iq">>,
|
||||
attrs = [{<<"type">>, iq_type_to_string(Type)}],
|
||||
children = SubEl}
|
||||
children = Children}
|
||||
end.
|
||||
|
||||
-spec parse_xdata_submit(El :: xmlel()) ->
|
||||
@ -579,33 +584,8 @@ add_delay_info(El, From, Time) ->
|
||||
binary()) -> xmlel().
|
||||
|
||||
add_delay_info(El, From, Time, Desc) ->
|
||||
case fxml:get_subtag_with_xmlns(El, <<"delay">>, ?NS_DELAY) of
|
||||
false ->
|
||||
%% Add new tag
|
||||
DelayTag = create_delay_tag(Time, From, Desc),
|
||||
fxml:append_subtags(El, [DelayTag]);
|
||||
DelayTag ->
|
||||
%% Update existing tag
|
||||
NewDelayTag =
|
||||
case {fxml:get_tag_cdata(DelayTag), Desc} of
|
||||
{<<"">>, <<"">>} ->
|
||||
DelayTag;
|
||||
{OldDesc, <<"">>} ->
|
||||
DelayTag#xmlel{children = [{xmlcdata, OldDesc}]};
|
||||
{<<"">>, NewDesc} ->
|
||||
DelayTag#xmlel{children = [{xmlcdata, NewDesc}]};
|
||||
{OldDesc, NewDesc} ->
|
||||
case binary:match(OldDesc, NewDesc) of
|
||||
nomatch ->
|
||||
FinalDesc = <<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.
|
||||
|
@ -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) ->
|
||||
|
@ -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),
|
||||
|
@ -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) ->
|
||||
|
@ -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().
|
||||
|
||||
|
@ -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
325
src/mod_delegation.erl
Normal 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.
|
@ -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) ->
|
||||
|
@ -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].
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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) ->
|
||||
|
105
src/mod_mam.erl
105
src/mod_mam.erl
@ -106,15 +106,12 @@ start(Host, Opts) ->
|
||||
ejabberd_hooks:add(anonymous_purge_hook, Host, ?MODULE,
|
||||
remove_user, 50),
|
||||
case gen_mod:get_opt(assume_mam_usage, Opts,
|
||||
fun(if_enabled) -> if_enabled;
|
||||
(on_request) -> on_request;
|
||||
(never) -> never
|
||||
end, never) of
|
||||
never ->
|
||||
ok;
|
||||
_ ->
|
||||
fun(B) when is_boolean(B) -> B end, false) of
|
||||
true ->
|
||||
ejabberd_hooks:add(message_is_archived, Host, ?MODULE,
|
||||
message_is_archived, 50)
|
||||
message_is_archived, 50);
|
||||
false ->
|
||||
ok
|
||||
end,
|
||||
ejabberd_commands:register_commands(get_commands_spec()),
|
||||
ok.
|
||||
@ -159,15 +156,12 @@ stop(Host) ->
|
||||
ejabberd_hooks:delete(anonymous_purge_hook, Host,
|
||||
?MODULE, remove_user, 50),
|
||||
case gen_mod:get_module_opt(Host, ?MODULE, assume_mam_usage,
|
||||
fun(if_enabled) -> if_enabled;
|
||||
(on_request) -> on_request;
|
||||
(never) -> never
|
||||
end, never) of
|
||||
never ->
|
||||
ok;
|
||||
_ ->
|
||||
fun(B) when is_boolean(B) -> B end, false) of
|
||||
true ->
|
||||
ejabberd_hooks:delete(message_is_archived, Host, ?MODULE,
|
||||
message_is_archived, 50)
|
||||
message_is_archived, 50);
|
||||
false ->
|
||||
ok
|
||||
end,
|
||||
ejabberd_commands:unregister_commands(get_commands_spec()),
|
||||
ok.
|
||||
@ -367,32 +361,13 @@ message_is_archived(true, _C2SState, _Peer, _JID, _Pkt) ->
|
||||
true;
|
||||
message_is_archived(false, C2SState, Peer,
|
||||
#jid{luser = LUser, lserver = LServer}, Pkt) ->
|
||||
Res = case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage,
|
||||
fun(if_enabled) -> if_enabled;
|
||||
(on_request) -> on_request;
|
||||
(never) -> never
|
||||
end, never) of
|
||||
if_enabled ->
|
||||
case get_prefs(LUser, LServer) of
|
||||
#archive_prefs{} = P ->
|
||||
{ok, P};
|
||||
error ->
|
||||
error
|
||||
end;
|
||||
on_request ->
|
||||
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
||||
cache_tab:lookup(archive_prefs, {LUser, LServer},
|
||||
fun() ->
|
||||
Mod:get_prefs(LUser, LServer)
|
||||
end);
|
||||
never ->
|
||||
error
|
||||
end,
|
||||
case Res of
|
||||
{ok, Prefs} ->
|
||||
case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage,
|
||||
fun(B) when is_boolean(B) -> B end, false) of
|
||||
true ->
|
||||
should_archive(strip_my_archived_tag(Pkt, LServer), LServer)
|
||||
andalso should_archive_peer(C2SState, Prefs, Peer);
|
||||
error ->
|
||||
andalso should_archive_peer(C2SState, get_prefs(LUser, LServer),
|
||||
Peer);
|
||||
false ->
|
||||
false
|
||||
end.
|
||||
|
||||
@ -493,9 +468,10 @@ process_iq(LServer, #iq{from = #jid{luser = LUser}, lang = Lang,
|
||||
xmpp:make_error(IQ, Err)
|
||||
end.
|
||||
|
||||
should_archive(#message{type = T}, _LServer) when T == error; T == result ->
|
||||
should_archive(#message{type = error}, _LServer) ->
|
||||
false;
|
||||
should_archive(#message{body = Body} = Pkt, LServer) ->
|
||||
should_archive(#message{body = Body, subject = Subject,
|
||||
type = Type} = Pkt, LServer) ->
|
||||
case is_resent(Pkt, LServer) of
|
||||
true ->
|
||||
false;
|
||||
@ -505,14 +481,11 @@ should_archive(#message{body = Body} = Pkt, LServer) ->
|
||||
true;
|
||||
no_store ->
|
||||
false;
|
||||
none ->
|
||||
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) ->
|
||||
|
@ -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) ->
|
||||
|
@ -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].
|
||||
|
@ -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,
|
||||
[
|
||||
|
@ -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) ->
|
||||
|
@ -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) ->
|
||||
|
@ -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
|
||||
|
@ -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 ->
|
||||
|
@ -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
348
src/mod_privilege.erl
Normal 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.
|
@ -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,
|
||||
|
@ -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)).
|
||||
|
@ -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) ->
|
||||
|
@ -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),
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user