lygneo-vines 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +7 -0
- data/Rakefile +23 -0
- data/bin/vines +4 -0
- data/conf/certs/README +39 -0
- data/conf/certs/ca-bundle.crt +3895 -0
- data/conf/config.rb +42 -0
- data/lib/vines/cli.rb +132 -0
- data/lib/vines/cluster/connection.rb +26 -0
- data/lib/vines/cluster/publisher.rb +55 -0
- data/lib/vines/cluster/pubsub.rb +92 -0
- data/lib/vines/cluster/sessions.rb +125 -0
- data/lib/vines/cluster/subscriber.rb +108 -0
- data/lib/vines/cluster.rb +246 -0
- data/lib/vines/command/bcrypt.rb +12 -0
- data/lib/vines/command/cert.rb +50 -0
- data/lib/vines/command/init.rb +68 -0
- data/lib/vines/command/ldap.rb +38 -0
- data/lib/vines/command/restart.rb +12 -0
- data/lib/vines/command/schema.rb +24 -0
- data/lib/vines/command/start.rb +28 -0
- data/lib/vines/command/stop.rb +18 -0
- data/lib/vines/config/host.rb +125 -0
- data/lib/vines/config/port.rb +132 -0
- data/lib/vines/config/pubsub.rb +108 -0
- data/lib/vines/config.rb +223 -0
- data/lib/vines/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -0
- data/lib/vines/follower.rb +111 -0
- data/lib/vines/jid.rb +95 -0
- data/lib/vines/kit.rb +23 -0
- data/lib/vines/log.rb +24 -0
- data/lib/vines/router.rb +179 -0
- data/lib/vines/stanza/iq/auth.rb +18 -0
- data/lib/vines/stanza/iq/disco_info.rb +45 -0
- data/lib/vines/stanza/iq/disco_items.rb +29 -0
- data/lib/vines/stanza/iq/error.rb +16 -0
- data/lib/vines/stanza/iq/ping.rb +16 -0
- data/lib/vines/stanza/iq/private_storage.rb +83 -0
- data/lib/vines/stanza/iq/query.rb +10 -0
- data/lib/vines/stanza/iq/result.rb +16 -0
- data/lib/vines/stanza/iq/roster.rb +140 -0
- data/lib/vines/stanza/iq/session.rb +17 -0
- data/lib/vines/stanza/iq/vcard.rb +56 -0
- data/lib/vines/stanza/iq/version.rb +25 -0
- data/lib/vines/stanza/iq.rb +48 -0
- data/lib/vines/stanza/message.rb +40 -0
- data/lib/vines/stanza/presence/error.rb +23 -0
- data/lib/vines/stanza/presence/probe.rb +37 -0
- data/lib/vines/stanza/presence/subscribe.rb +42 -0
- data/lib/vines/stanza/presence/subscribed.rb +51 -0
- data/lib/vines/stanza/presence/unavailable.rb +15 -0
- data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
- data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
- data/lib/vines/stanza/presence.rb +141 -0
- data/lib/vines/stanza/pubsub/create.rb +39 -0
- data/lib/vines/stanza/pubsub/delete.rb +41 -0
- data/lib/vines/stanza/pubsub/publish.rb +66 -0
- data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
- data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
- data/lib/vines/stanza/pubsub.rb +22 -0
- data/lib/vines/stanza.rb +175 -0
- data/lib/vines/storage/ldap.rb +71 -0
- data/lib/vines/storage/local.rb +139 -0
- data/lib/vines/storage/null.rb +39 -0
- data/lib/vines/storage/sql.rb +138 -0
- data/lib/vines/storage.rb +239 -0
- data/lib/vines/store.rb +110 -0
- data/lib/vines/stream/client/auth.rb +74 -0
- data/lib/vines/stream/client/auth_restart.rb +29 -0
- data/lib/vines/stream/client/bind.rb +72 -0
- data/lib/vines/stream/client/bind_restart.rb +24 -0
- data/lib/vines/stream/client/closed.rb +13 -0
- data/lib/vines/stream/client/ready.rb +17 -0
- data/lib/vines/stream/client/session.rb +210 -0
- data/lib/vines/stream/client/start.rb +27 -0
- data/lib/vines/stream/client/tls.rb +38 -0
- data/lib/vines/stream/client.rb +84 -0
- data/lib/vines/stream/component/handshake.rb +26 -0
- data/lib/vines/stream/component/ready.rb +23 -0
- data/lib/vines/stream/component/start.rb +19 -0
- data/lib/vines/stream/component.rb +58 -0
- data/lib/vines/stream/http/auth.rb +22 -0
- data/lib/vines/stream/http/bind.rb +32 -0
- data/lib/vines/stream/http/bind_restart.rb +37 -0
- data/lib/vines/stream/http/ready.rb +29 -0
- data/lib/vines/stream/http/request.rb +172 -0
- data/lib/vines/stream/http/session.rb +120 -0
- data/lib/vines/stream/http/sessions.rb +65 -0
- data/lib/vines/stream/http/start.rb +23 -0
- data/lib/vines/stream/http.rb +157 -0
- data/lib/vines/stream/parser.rb +79 -0
- data/lib/vines/stream/sasl.rb +128 -0
- data/lib/vines/stream/server/auth.rb +13 -0
- data/lib/vines/stream/server/auth_restart.rb +13 -0
- data/lib/vines/stream/server/final_restart.rb +21 -0
- data/lib/vines/stream/server/outbound/auth.rb +31 -0
- data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
- data/lib/vines/stream/server/outbound/final_features.rb +28 -0
- data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/start.rb +20 -0
- data/lib/vines/stream/server/outbound/tls.rb +30 -0
- data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
- data/lib/vines/stream/server/ready.rb +24 -0
- data/lib/vines/stream/server/start.rb +13 -0
- data/lib/vines/stream/server/tls.rb +13 -0
- data/lib/vines/stream/server.rb +150 -0
- data/lib/vines/stream/state.rb +60 -0
- data/lib/vines/stream.rb +247 -0
- data/lib/vines/token_bucket.rb +55 -0
- data/lib/vines/user.rb +123 -0
- data/lib/vines/version.rb +6 -0
- data/lib/vines/xmpp_server.rb +25 -0
- data/lib/vines.rb +203 -0
- data/test/cluster/publisher_test.rb +57 -0
- data/test/cluster/sessions_test.rb +47 -0
- data/test/cluster/subscriber_test.rb +109 -0
- data/test/config/host_test.rb +369 -0
- data/test/config/pubsub_test.rb +187 -0
- data/test/config_test.rb +732 -0
- data/test/error_test.rb +58 -0
- data/test/ext/nokogiri.rb +14 -0
- data/test/follower_test.rb +102 -0
- data/test/jid_test.rb +147 -0
- data/test/kit_test.rb +31 -0
- data/test/router_test.rb +243 -0
- data/test/stanza/iq/disco_info_test.rb +78 -0
- data/test/stanza/iq/disco_items_test.rb +49 -0
- data/test/stanza/iq/private_storage_test.rb +184 -0
- data/test/stanza/iq/roster_test.rb +229 -0
- data/test/stanza/iq/session_test.rb +25 -0
- data/test/stanza/iq/vcard_test.rb +146 -0
- data/test/stanza/iq/version_test.rb +64 -0
- data/test/stanza/iq_test.rb +70 -0
- data/test/stanza/message_test.rb +126 -0
- data/test/stanza/presence/probe_test.rb +50 -0
- data/test/stanza/presence/subscribe_test.rb +83 -0
- data/test/stanza/pubsub/create_test.rb +116 -0
- data/test/stanza/pubsub/delete_test.rb +169 -0
- data/test/stanza/pubsub/publish_test.rb +309 -0
- data/test/stanza/pubsub/subscribe_test.rb +205 -0
- data/test/stanza/pubsub/unsubscribe_test.rb +148 -0
- data/test/stanza_test.rb +85 -0
- data/test/storage/ldap_test.rb +201 -0
- data/test/storage/local_test.rb +59 -0
- data/test/storage/mock_redis.rb +97 -0
- data/test/storage/null_test.rb +29 -0
- data/test/storage/storage_tests.rb +182 -0
- data/test/storage_test.rb +85 -0
- data/test/store_test.rb +130 -0
- data/test/stream/client/auth_test.rb +137 -0
- data/test/stream/client/ready_test.rb +47 -0
- data/test/stream/client/session_test.rb +27 -0
- data/test/stream/component/handshake_test.rb +52 -0
- data/test/stream/component/ready_test.rb +103 -0
- data/test/stream/component/start_test.rb +39 -0
- data/test/stream/http/auth_test.rb +70 -0
- data/test/stream/http/ready_test.rb +86 -0
- data/test/stream/http/request_test.rb +209 -0
- data/test/stream/http/sessions_test.rb +49 -0
- data/test/stream/http/start_test.rb +50 -0
- data/test/stream/parser_test.rb +122 -0
- data/test/stream/sasl_test.rb +195 -0
- data/test/stream/server/auth_test.rb +61 -0
- data/test/stream/server/outbound/auth_test.rb +75 -0
- data/test/stream/server/ready_test.rb +98 -0
- data/test/test_helper.rb +42 -0
- data/test/token_bucket_test.rb +44 -0
- data/test/user_test.rb +96 -0
- data/vines.gemspec +30 -0
- metadata +387 -0
data/conf/config.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
Vines::Config.configure do
|
2
|
+
# Set the logging level to debug, info, warn, error, or fatal. The debug
|
3
|
+
# level logs all XML sent and received by the server.
|
4
|
+
log :info
|
5
|
+
|
6
|
+
# Set the directory in which to look for virtual hosts' TLS certificates.
|
7
|
+
# This is optional and defaults to the conf/certs directory created during
|
8
|
+
# `vines init`.
|
9
|
+
#certs 'config/vines'
|
10
|
+
|
11
|
+
# Setup a pepper to generate the encrypted password.
|
12
|
+
pepper "065eb8798b181ff0ea2c5c16aee0ff8b70e04e2ee6bd6e08b49da46924223e39127d5335e466207d42bf2a045c12be5f90e92012a4f05f7fc6d9f3c875f4c95b"
|
13
|
+
|
14
|
+
host 'lygneo' do
|
15
|
+
storage 'sql'
|
16
|
+
end
|
17
|
+
|
18
|
+
# Configure the client-to-server port. The max_resources_per_account attribute
|
19
|
+
# limits how many concurrent connections one user can have to the server.
|
20
|
+
client '0.0.0.0', 5222 do
|
21
|
+
max_stanza_size 65536
|
22
|
+
max_resources_per_account 5
|
23
|
+
end
|
24
|
+
|
25
|
+
# Configure the server-to-server port. The max_stanza_size attribute should be
|
26
|
+
# much larger than the setting for client-to-server.
|
27
|
+
server '0.0.0.0', 5269 do
|
28
|
+
max_stanza_size 131072
|
29
|
+
hosts []
|
30
|
+
end
|
31
|
+
|
32
|
+
# Configure the built-in HTTP server that serves static files and responds to
|
33
|
+
# XEP-0124 BOSH requests. This allows HTTP clients to connect to
|
34
|
+
# the XMPP server.
|
35
|
+
http '0.0.0.0', 5280 do
|
36
|
+
bind '/xmpp'
|
37
|
+
max_stanza_size 65536
|
38
|
+
max_resources_per_account 5
|
39
|
+
root 'public'
|
40
|
+
vroute ''
|
41
|
+
end
|
42
|
+
end
|
data/lib/vines/cli.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
module Vines
|
2
|
+
# The command line application that's invoked by the `vines` binary included
|
3
|
+
# in the gem. Parses the command line arguments to create a new server
|
4
|
+
# directory, and starts and stops the server.
|
5
|
+
class CLI
|
6
|
+
COMMANDS = %w[start stop restart init bcrypt cert ldap schema]
|
7
|
+
|
8
|
+
def self.start
|
9
|
+
self.new.start
|
10
|
+
end
|
11
|
+
|
12
|
+
# Run the command line application to parse arguments and run sub-commands.
|
13
|
+
# Exits the process with a non-zero return code to indicate failure.
|
14
|
+
#
|
15
|
+
# Returns nothing.
|
16
|
+
def start
|
17
|
+
register_storage
|
18
|
+
opts = parse(ARGV)
|
19
|
+
check_config(opts)
|
20
|
+
command = Command.const_get(opts[:command].capitalize).new
|
21
|
+
begin
|
22
|
+
command.run(opts)
|
23
|
+
rescue SystemExit
|
24
|
+
# do nothing
|
25
|
+
rescue Exception => e
|
26
|
+
puts e.message
|
27
|
+
exit(1)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Try to load various storage backends provided by vines-* gems and register
|
34
|
+
# them with the storage system for the config file to use.
|
35
|
+
#
|
36
|
+
# Returns nothing.
|
37
|
+
def register_storage
|
38
|
+
%w[couchdb mongodb redis sql].each do |backend|
|
39
|
+
begin
|
40
|
+
require 'vines/storage/%s' % backend
|
41
|
+
rescue LoadError
|
42
|
+
# do nothing
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Parse the command line arguments and run the matching sub-command
|
48
|
+
# (e.g. init, start, stop, etc).
|
49
|
+
#
|
50
|
+
# args - The ARGV array provided by the command line.
|
51
|
+
#
|
52
|
+
# Returns nothing.
|
53
|
+
def parse(args)
|
54
|
+
options = {}
|
55
|
+
parser = OptionParser.new do |opts|
|
56
|
+
opts.banner = "Usage: vines [options] #{COMMANDS.join('|')}"
|
57
|
+
|
58
|
+
opts.separator ""
|
59
|
+
opts.separator "Daemon options:"
|
60
|
+
|
61
|
+
opts.on('-d', '--daemonize', 'Run daemonized in the background') do |daemonize|
|
62
|
+
options[:daemonize] = daemonize
|
63
|
+
end
|
64
|
+
|
65
|
+
options[:log] = 'log/vines.log'
|
66
|
+
opts.on('-l', '--log FILE', 'File to redirect output (default: log/vines.log)') do |log|
|
67
|
+
options[:log] = log
|
68
|
+
end
|
69
|
+
|
70
|
+
options[:pid] = 'pid/vines.pid'
|
71
|
+
opts.on('-P', '--pid FILE', 'File to store PID (default: pid/vines.pid)') do |pid|
|
72
|
+
options[:pid] = pid
|
73
|
+
end
|
74
|
+
|
75
|
+
opts.separator ""
|
76
|
+
opts.separator "Common options:"
|
77
|
+
|
78
|
+
opts.on('-h', '--help', 'Show this message') do |help|
|
79
|
+
options[:help] = help
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.on('-v', '--version', 'Show version') do |version|
|
83
|
+
options[:version] = version
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
begin
|
88
|
+
parser.parse!(args)
|
89
|
+
rescue
|
90
|
+
puts parser
|
91
|
+
exit(1)
|
92
|
+
end
|
93
|
+
|
94
|
+
if options[:version]
|
95
|
+
puts Vines::VERSION
|
96
|
+
exit
|
97
|
+
end
|
98
|
+
|
99
|
+
if options[:help]
|
100
|
+
puts parser
|
101
|
+
exit
|
102
|
+
end
|
103
|
+
|
104
|
+
command = args.shift
|
105
|
+
unless COMMANDS.include?(command)
|
106
|
+
puts parser
|
107
|
+
exit(1)
|
108
|
+
end
|
109
|
+
|
110
|
+
options.tap do |opts|
|
111
|
+
opts[:args] = args
|
112
|
+
opts[:command] = command
|
113
|
+
opts[:config] = File.expand_path("#{Dir.pwd}/config/vines.rb") || File.expand_path("conf/config.rb")
|
114
|
+
opts[:pid] = File.expand_path(opts[:pid])
|
115
|
+
opts[:log] = File.expand_path(opts[:log])
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Many commands must be run in the context of a vines server directory
|
120
|
+
# created with `vines init`. If the command can't find the server's config
|
121
|
+
# file, print an error message and exit.
|
122
|
+
#
|
123
|
+
# Returns nothing.
|
124
|
+
def check_config(opts)
|
125
|
+
return if %w[bcrypt init].include?(opts[:command])
|
126
|
+
unless File.exists?(opts[:config])
|
127
|
+
puts "No config file found at #{opts[:config]}"
|
128
|
+
exit(1)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Cluster
|
5
|
+
# Create and cache a redis database connection.
|
6
|
+
class Connection
|
7
|
+
attr_accessor :host, :port, :database, :password
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@redis, @host, @port, @database, @password = nil, nil, nil, nil, nil
|
11
|
+
end
|
12
|
+
|
13
|
+
# Return a shared redis connection.
|
14
|
+
def connect
|
15
|
+
@redis ||= create
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return a new redis connection.
|
19
|
+
def create
|
20
|
+
conn = EM::Hiredis::Client.new(@host, @port, @password, @database)
|
21
|
+
conn.connect
|
22
|
+
conn
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Cluster
|
5
|
+
# Broadcast messages to other cluster nodes via redis pubsub channels. All
|
6
|
+
# members subscribe to a channel for heartbeats, online, and offline
|
7
|
+
# messages from other nodes. This allows new nodes to be added to the
|
8
|
+
# cluster dynamically, without configuring all other nodes.
|
9
|
+
class Publisher
|
10
|
+
include Vines::Log
|
11
|
+
|
12
|
+
ALL, STANZA, USER = %w[cluster:nodes:all stanza user].map {|s| s.freeze }
|
13
|
+
|
14
|
+
def initialize(cluster)
|
15
|
+
@cluster = cluster
|
16
|
+
end
|
17
|
+
|
18
|
+
# Publish a :heartbeat, :online, or :offline message to the nodes:all
|
19
|
+
# broadcast channel.
|
20
|
+
def broadcast(type)
|
21
|
+
redis.publish(ALL, {
|
22
|
+
from: @cluster.id,
|
23
|
+
type: type,
|
24
|
+
time: Time.now.to_i
|
25
|
+
}.to_json)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Send the stanza to the node hosting the user's session. The stanza is
|
29
|
+
# published to the channel to which the remote node is listening for
|
30
|
+
# messages.
|
31
|
+
def route(stanza, node)
|
32
|
+
log.debug { "Sent cluster stanza: %s -> %s\n%s\n" % [@cluster.id, node, stanza] }
|
33
|
+
redis.publish("cluster:nodes:#{node}", {
|
34
|
+
from: @cluster.id,
|
35
|
+
type: STANZA,
|
36
|
+
stanza: stanza.to_s
|
37
|
+
}.to_json)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Notify the remote node that the user's roster has changed and it should
|
41
|
+
# reload the user from storage.
|
42
|
+
def update_user(jid, node)
|
43
|
+
redis.publish("cluster:nodes:#{node}", {
|
44
|
+
from: @cluster.id,
|
45
|
+
type: USER,
|
46
|
+
jid: jid.to_s
|
47
|
+
}.to_json)
|
48
|
+
end
|
49
|
+
|
50
|
+
def redis
|
51
|
+
@cluster.connection
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Cluster
|
5
|
+
# Manages the pubsub topic list and subscribers stored in redis. When a
|
6
|
+
# message is published to a topic, the receiving cluster node broadcasts
|
7
|
+
# the message to all subscribers at all other cluster nodes.
|
8
|
+
class PubSub
|
9
|
+
def initialize(cluster)
|
10
|
+
@cluster = cluster
|
11
|
+
end
|
12
|
+
|
13
|
+
# Create a pubsub topic (a.k.a. node), in the given domain, to which
|
14
|
+
# messages may be published. The domain argument will be one of the
|
15
|
+
# configured pubsub subdomains in conf/config.rb (e.g. games.wonderland.lit,
|
16
|
+
# topics.wonderland.lit, etc).
|
17
|
+
def add_node(domain, node)
|
18
|
+
redis.sadd("pubsub:#{domain}:nodes", node)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Remove a pubsub topic so messages may no longer be broadcast to it.
|
22
|
+
def delete_node(domain, node)
|
23
|
+
redis.smembers("pubsub:#{domain}:subscribers_#{node}") do |subscribers|
|
24
|
+
redis.multi
|
25
|
+
subscribers.each do |jid|
|
26
|
+
redis.srem("pubsub:#{domain}:subscriptions_#{jid}", node)
|
27
|
+
end
|
28
|
+
redis.del("pubsub:#{domain}:subscribers_#{node}")
|
29
|
+
redis.srem("pubsub:#{domain}:nodes", node)
|
30
|
+
redis.exec
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Subscribe the JID to the pubsub topic so it will receive any messages
|
35
|
+
# published to it.
|
36
|
+
def subscribe(domain, node, jid)
|
37
|
+
jid = JID.new(jid)
|
38
|
+
redis.multi
|
39
|
+
redis.sadd("pubsub:#{domain}:subscribers_#{node}", jid.to_s)
|
40
|
+
redis.sadd("pubsub:#{domain}:subscriptions_#{jid}", node)
|
41
|
+
redis.exec
|
42
|
+
end
|
43
|
+
|
44
|
+
# Unsubscribe the JID from the pubsub topic, deregistering its interest
|
45
|
+
# in receiving any messages published to it.
|
46
|
+
def unsubscribe(domain, node, jid)
|
47
|
+
jid = JID.new(jid)
|
48
|
+
redis.multi
|
49
|
+
redis.srem("pubsub:#{domain}:subscribers_#{node}", jid.to_s)
|
50
|
+
redis.srem("pubsub:#{domain}:subscriptions_#{jid}", node)
|
51
|
+
redis.exec
|
52
|
+
redis.scard("pubsub:#{domain}:subscribers_#{node}") do |count|
|
53
|
+
delete_node(domain, node) if count == 0
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Unsubscribe the JID from all pubsub topics. This is useful when the
|
58
|
+
# JID's session ends by logout or disconnect.
|
59
|
+
def unsubscribe_all(domain, jid)
|
60
|
+
jid = JID.new(jid)
|
61
|
+
redis.smembers("pubsub:#{domain}:subscriptions_#{jid}") do |nodes|
|
62
|
+
nodes.each do |node|
|
63
|
+
unsubscribe(domain, node, jid)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return true if the pubsub topic exists and messages may be published to it.
|
69
|
+
def node?(domain, node)
|
70
|
+
@cluster.query(:sismember, "pubsub:#{domain}:nodes", node) == 1
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return true if the JID is a registered subscriber to the pubsub topic and
|
74
|
+
# messages published to it should be routed to the JID.
|
75
|
+
def subscribed?(domain, node, jid)
|
76
|
+
jid = JID.new(jid)
|
77
|
+
@cluster.query(:sismember, "pubsub:#{domain}:subscribers_#{node}", jid.to_s) == 1
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return a list of JIDs subscribed to the pubsub topic.
|
81
|
+
def subscribers(domain, node)
|
82
|
+
@cluster.query(:smembers, "pubsub:#{domain}:subscribers_#{node}")
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def redis
|
88
|
+
@cluster.connection
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Cluster
|
5
|
+
# Manages the cluster node list and user session routing table stored in
|
6
|
+
# redis. All cluster nodes share this in-memory database to quickly discover
|
7
|
+
# the node hosting a particular user session. Once a session is located,
|
8
|
+
# stanzas can be routed to that node via the +Publisher+.
|
9
|
+
class Sessions
|
10
|
+
include Vines::Log
|
11
|
+
|
12
|
+
NODES = 'cluster:nodes'.freeze
|
13
|
+
|
14
|
+
def initialize(cluster)
|
15
|
+
@cluster, @nodes = cluster, {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return the sessions for these JIDs. If a bare JID is used, all sessions
|
19
|
+
# for that user will be returned. If a full JID is used, the session for
|
20
|
+
# that single connected stream is returned.
|
21
|
+
def find(*jids)
|
22
|
+
jids.flatten.map do |jid|
|
23
|
+
jid = JID.new(jid)
|
24
|
+
jid.bare? ? user_sessions(jid) : user_session(jid)
|
25
|
+
end.compact.flatten
|
26
|
+
end
|
27
|
+
|
28
|
+
# Persist the user's session to the shared redis cache so that other cluster
|
29
|
+
# nodes can locate the node hosting this user's connection and route messages
|
30
|
+
# to them.
|
31
|
+
def save(jid, attrs)
|
32
|
+
jid = JID.new(jid)
|
33
|
+
session = {node: @cluster.id}.merge(attrs)
|
34
|
+
redis.multi
|
35
|
+
redis.hset("sessions:#{jid.bare}", jid.resource, session.to_json)
|
36
|
+
redis.sadd("cluster:nodes:#{@cluster.id}", jid.to_s)
|
37
|
+
redis.exec
|
38
|
+
end
|
39
|
+
|
40
|
+
# Remove this user from the cluster routing table so that no further stanzas
|
41
|
+
# may be routed to them. This must be called when the user's session is
|
42
|
+
# terminated, either by logout or stream disconnect.
|
43
|
+
def delete(jid)
|
44
|
+
jid = JID.new(jid)
|
45
|
+
redis.hget("sessions:#{jid.bare}", jid.resource) do |response|
|
46
|
+
if doc = JSON.parse(response) rescue nil
|
47
|
+
redis.multi
|
48
|
+
redis.hdel("sessions:#{jid.bare}", jid.resource)
|
49
|
+
redis.srem("cluster:nodes:#{doc['node']}", jid.to_s)
|
50
|
+
redis.exec
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Remove all user sessions from the routing table associated with the
|
56
|
+
# given node ID. Cluster nodes call this themselves during normal shutdown.
|
57
|
+
# However, if a node dies without being properly shutdown, the other nodes
|
58
|
+
# will cleanup its sessions when they detect the node is offline.
|
59
|
+
def delete_all(node)
|
60
|
+
@nodes.delete(node)
|
61
|
+
redis.smembers("cluster:nodes:#{node}") do |jids|
|
62
|
+
redis.multi
|
63
|
+
redis.del("cluster:nodes:#{node}")
|
64
|
+
redis.hdel(NODES, node)
|
65
|
+
jids.each do |jid|
|
66
|
+
jid = JID.new(jid)
|
67
|
+
redis.hdel("sessions:#{jid.bare}", jid.resource)
|
68
|
+
end
|
69
|
+
redis.exec
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Cluster nodes broadcast a heartbeat to other members every second. If we
|
74
|
+
# haven't heard from a node in five seconds, assume it's offline and cleanup
|
75
|
+
# its session cache for it. Nodes may die abrubtly, without a chance to clear
|
76
|
+
# their sessions, so other members cleanup for them.
|
77
|
+
def expire
|
78
|
+
redis.hset(NODES, @cluster.id, Time.now.to_i)
|
79
|
+
redis.hgetall(NODES) do |response|
|
80
|
+
now = Time.now
|
81
|
+
expired = Hash[*response].select do |node, active|
|
82
|
+
offset = @nodes[node] || 0
|
83
|
+
(now - offset) - Time.at(active.to_i) > 5
|
84
|
+
end.keys
|
85
|
+
expired.each {|node| delete_all(node) }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Notify the session store that this node is still alive. The node
|
90
|
+
# broadcasts its current time, so all cluster members' clocks don't
|
91
|
+
# necessarily need to be in sync.
|
92
|
+
def poke(node, time)
|
93
|
+
offset = Time.now.to_i - time
|
94
|
+
@nodes[node] = offset
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# Return all remote sessions for this user's bare JID.
|
100
|
+
def user_sessions(jid)
|
101
|
+
response = @cluster.query(:hgetall, "sessions:#{jid.bare}") || []
|
102
|
+
Hash[*response].map do |resource, json|
|
103
|
+
if session = JSON.parse(json) rescue nil
|
104
|
+
session['jid'] = JID.new(jid.node, jid.domain, resource).to_s
|
105
|
+
end
|
106
|
+
session
|
107
|
+
end.compact.reject {|session| session['node'] == @cluster.id }
|
108
|
+
end
|
109
|
+
|
110
|
+
# Return the remote session for this full JID or nil if not found.
|
111
|
+
def user_session(jid)
|
112
|
+
response = @cluster.query(:hget, "sessions:#{jid.bare}", jid.resource)
|
113
|
+
return unless response
|
114
|
+
session = JSON.parse(response) rescue nil
|
115
|
+
return if session.nil? || session['node'] == @cluster.id
|
116
|
+
session['jid'] = jid.to_s
|
117
|
+
session
|
118
|
+
end
|
119
|
+
|
120
|
+
def redis
|
121
|
+
@cluster.connection
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Cluster
|
5
|
+
# Subscribes to the redis nodes:all broadcast channel to listen for
|
6
|
+
# heartbeats from other cluster members. Also subscribes to a channel
|
7
|
+
# exclusively for this particular node, listening for stanzas routed to us
|
8
|
+
# from other nodes.
|
9
|
+
class Subscriber
|
10
|
+
include Vines::Log
|
11
|
+
|
12
|
+
ALL, FROM, HEARTBEAT, OFFLINE, ONLINE, STANZA, TIME, TO, TYPE, USER =
|
13
|
+
%w[cluster:nodes:all from heartbeat offline online stanza time to type user].map {|s| s.freeze }
|
14
|
+
|
15
|
+
def initialize(cluster)
|
16
|
+
@cluster = cluster
|
17
|
+
@channel = "cluster:nodes:#{@cluster.id}"
|
18
|
+
@messages = EM::Queue.new
|
19
|
+
process_messages
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create a new redis connection and subscribe to the nodes:all broadcast
|
23
|
+
# channel as well as the channel for this cluster node. Redis connections
|
24
|
+
# in subscribe mode cannot be used for other key/value operations.
|
25
|
+
def subscribe
|
26
|
+
conn = @cluster.connect
|
27
|
+
conn.subscribe(ALL)
|
28
|
+
conn.subscribe(@channel)
|
29
|
+
conn.on(:message) do |channel, message|
|
30
|
+
@messages.push([channel, message])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Recursively process incoming messages from the queue, guaranteeing they
|
37
|
+
# are processed in the order they are received.
|
38
|
+
def process_messages
|
39
|
+
@messages.pop do |channel, message|
|
40
|
+
Fiber.new do
|
41
|
+
on_message(channel, message)
|
42
|
+
process_messages
|
43
|
+
end.resume
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Process messages as they arrive on the pubsub channels to which we're
|
48
|
+
# subscribed.
|
49
|
+
def on_message(channel, message)
|
50
|
+
doc = JSON.parse(message)
|
51
|
+
case channel
|
52
|
+
when ALL then to_all(doc)
|
53
|
+
when @channel then to_node(doc)
|
54
|
+
end
|
55
|
+
rescue => e
|
56
|
+
log.error("Cluster subscription message failed: #{e}")
|
57
|
+
end
|
58
|
+
|
59
|
+
# Process a message sent to the nodes:all broadcast channel. In the case
|
60
|
+
# of node heartbeats, we update the last time we heard from this node so
|
61
|
+
# we can cleanup its session if it goes offline.
|
62
|
+
def to_all(message)
|
63
|
+
case message[TYPE]
|
64
|
+
when ONLINE, HEARTBEAT
|
65
|
+
@cluster.poke(message[FROM], message[TIME])
|
66
|
+
when OFFLINE
|
67
|
+
@cluster.delete_sessions(message[FROM])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Process a message published to this node's channel. Messages sent to
|
72
|
+
# this channel are stanzas that need to be routed to connections attached
|
73
|
+
# to this node.
|
74
|
+
def to_node(message)
|
75
|
+
case message[TYPE]
|
76
|
+
when STANZA then route_stanza(message)
|
77
|
+
when USER then update_user(message)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Send the stanza, from a remote cluster node, to locally connected
|
82
|
+
# streams for the destination user.
|
83
|
+
def route_stanza(message)
|
84
|
+
node = Nokogiri::XML(message[STANZA]).root rescue nil
|
85
|
+
return unless node
|
86
|
+
log.debug { "Received cluster stanza: %s -> %s\n%s\n" % [message[FROM], @cluster.id, node] }
|
87
|
+
if node[TO]
|
88
|
+
@cluster.connected_resources(node[TO]).each do |recipient|
|
89
|
+
recipient.write(node)
|
90
|
+
end
|
91
|
+
else
|
92
|
+
log.warn("Cluster stanza missing address:\n#{node}")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Update the roster information, that's cached in locally connected
|
97
|
+
# streams, for this user.
|
98
|
+
def update_user(message)
|
99
|
+
jid = JID.new(message['jid']).bare
|
100
|
+
if user = @cluster.storage(jid.domain).find_user(jid)
|
101
|
+
@cluster.connected_resources(jid).each do |stream|
|
102
|
+
stream.user.update_from(user)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|