diaspora-vines 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/contact.rb +111 -0
- data/lib/vines/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -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/contact_test.rb +102 -0
- data/test/error_test.rb +58 -0
- data/test/ext/nokogiri.rb +14 -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 'diaspora' 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
|