vinesmod 0.4.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|