vinesmod 0.4.5
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.
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +43 -0
- data/Rakefile +57 -0
- data/bin/vines +93 -0
- data/conf/certs/README +39 -0
- data/conf/certs/ca-bundle.crt +3366 -0
- data/conf/config.rb +149 -0
- data/lib/vines.rb +197 -0
- data/lib/vines/cluster.rb +246 -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/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/register.rb +27 -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/command/unregister.rb +27 -0
- data/lib/vines/config.rb +213 -0
- data/lib/vines/config/host.rb +119 -0
- data/lib/vines/config/port.rb +132 -0
- data/lib/vines/config/pubsub.rb +108 -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 +35 -0
- data/lib/vines/log.rb +24 -0
- data/lib/vines/router.rb +179 -0
- data/lib/vines/stanza.rb +175 -0
- data/lib/vines/stanza/iq.rb +48 -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/register.rb +42 -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/message.rb +43 -0
- data/lib/vines/stanza/presence.rb +156 -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/pubsub.rb +22 -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/storage.rb +188 -0
- data/lib/vines/storage/local.rb +165 -0
- data/lib/vines/storage/null.rb +39 -0
- data/lib/vines/storage/sql.rb +260 -0
- data/lib/vines/store.rb +94 -0
- data/lib/vines/stream.rb +247 -0
- data/lib/vines/stream/client.rb +84 -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/component.rb +58 -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/http.rb +157 -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/parser.rb +78 -0
- data/lib/vines/stream/sasl.rb +92 -0
- data/lib/vines/stream/server.rb +150 -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/state.rb +60 -0
- data/lib/vines/token_bucket.rb +55 -0
- data/lib/vines/user.rb +123 -0
- data/lib/vines/version.rb +5 -0
- data/lib/vines/xmpp_server.rb +43 -0
- data/vines.gemspec +36 -0
- data/web/404.html +51 -0
- data/web/apple-touch-icon.png +0 -0
- data/web/chat/coffeescripts/chat.coffee +362 -0
- data/web/chat/coffeescripts/init.coffee +15 -0
- data/web/chat/index.html +16 -0
- data/web/chat/javascripts/app.js +1 -0
- data/web/chat/stylesheets/chat.css +144 -0
- data/web/favicon.png +0 -0
- data/web/lib/coffeescripts/button.coffee +25 -0
- data/web/lib/coffeescripts/contact.coffee +32 -0
- data/web/lib/coffeescripts/filter.coffee +49 -0
- data/web/lib/coffeescripts/layout.coffee +30 -0
- data/web/lib/coffeescripts/login.coffee +68 -0
- data/web/lib/coffeescripts/logout.coffee +5 -0
- data/web/lib/coffeescripts/navbar.coffee +84 -0
- data/web/lib/coffeescripts/notification.coffee +14 -0
- data/web/lib/coffeescripts/router.coffee +40 -0
- data/web/lib/coffeescripts/session.coffee +229 -0
- data/web/lib/coffeescripts/transfer.coffee +106 -0
- data/web/lib/images/dark-gray.png +0 -0
- data/web/lib/images/default-user.png +0 -0
- data/web/lib/images/light-gray.png +0 -0
- data/web/lib/images/logo-large.png +0 -0
- data/web/lib/images/logo-small.png +0 -0
- data/web/lib/images/white.png +0 -0
- data/web/lib/javascripts/base.js +12 -0
- data/web/lib/javascripts/icons.js +110 -0
- data/web/lib/javascripts/jquery.cookie.js +91 -0
- data/web/lib/javascripts/jquery.js +4 -0
- data/web/lib/javascripts/raphael.js +6 -0
- data/web/lib/javascripts/strophe.js +1 -0
- data/web/lib/stylesheets/base.css +385 -0
- data/web/lib/stylesheets/login.css +68 -0
- metadata +423 -0
@@ -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
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Command
|
5
|
+
class Cert
|
6
|
+
def run(opts)
|
7
|
+
raise 'vines cert <domain>' unless opts[:args].size == 1
|
8
|
+
require opts[:config]
|
9
|
+
create_cert(opts[:args].first, Config.instance.certs)
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_cert(domain, dir)
|
13
|
+
domain = domain.downcase
|
14
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
15
|
+
ca = OpenSSL::X509::Name.parse("/C=US/ST=Colorado/L=Denver/O=Vines XMPP Server/CN=#{domain}")
|
16
|
+
cert = OpenSSL::X509::Certificate.new
|
17
|
+
cert.version = 2
|
18
|
+
cert.subject = ca
|
19
|
+
cert.issuer = ca
|
20
|
+
cert.serial = Time.now.to_i
|
21
|
+
cert.public_key = key.public_key
|
22
|
+
cert.not_before = Time.now - (24 * 60 * 60)
|
23
|
+
cert.not_after = Time.now + (365 * 24 * 60 * 60)
|
24
|
+
|
25
|
+
factory = OpenSSL::X509::ExtensionFactory.new
|
26
|
+
factory.subject_certificate = cert
|
27
|
+
factory.issuer_certificate = cert
|
28
|
+
cert.extensions = [
|
29
|
+
%w[basicConstraints CA:TRUE],
|
30
|
+
%w[subjectKeyIdentifier hash],
|
31
|
+
%w[subjectAltName] << [domain, hostname].map {|n| "DNS:#{n}" }.join(',')
|
32
|
+
].map {|k, v| factory.create_ext(k, v) }
|
33
|
+
|
34
|
+
cert.sign(key, OpenSSL::Digest::SHA1.new)
|
35
|
+
|
36
|
+
{'key' => key, 'crt' => cert}.each_pair do |ext, o|
|
37
|
+
name = File.join(dir, "#{domain}.#{ext}")
|
38
|
+
File.open(name, 'w:utf-8') {|f| f.write(o.to_pem) }
|
39
|
+
File.chmod(0600, name) if ext == 'key'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def hostname
|
46
|
+
Socket.gethostbyname(Socket.gethostname).first.downcase
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|