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
@@ -0,0 +1,246 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
# Server instances may be connected to one another in a cluster so they
|
5
|
+
# can host a single chat domain, or set of domains, across many servers,
|
6
|
+
# transparently to users. A redis database is used for the session routing
|
7
|
+
# table, mapping JIDs to their node's location. Redis pubsub channels are
|
8
|
+
# used to communicate amongst nodes.
|
9
|
+
#
|
10
|
+
# Using a shared in-memory cache, like redis, rather than synchronizing the
|
11
|
+
# cache to each node, allows us to add cluster nodes dynamically, without
|
12
|
+
# updating all other nodes' config files. It also greatly reduces the amount
|
13
|
+
# of memory required by the chat server processes.
|
14
|
+
class Cluster
|
15
|
+
include Vines::Log
|
16
|
+
|
17
|
+
attr_reader :id
|
18
|
+
|
19
|
+
%w[host port database password].each do |name|
|
20
|
+
define_method(name) do |*args|
|
21
|
+
if args.first
|
22
|
+
@connection.send("#{name}=", args.first)
|
23
|
+
else
|
24
|
+
@connection.send(name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(config, &block)
|
30
|
+
@config, @id = config, Kit.uuid
|
31
|
+
@connection = Connection.new
|
32
|
+
@sessions = Sessions.new(self)
|
33
|
+
@publisher = Publisher.new(self)
|
34
|
+
@subscriber = Subscriber.new(self)
|
35
|
+
@pubsub = PubSub.new(self)
|
36
|
+
instance_eval(&block)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Join this node to the cluster by broadcasting its state to the
|
40
|
+
# other nodes, subscribing to redis channels, and scheduling periodic
|
41
|
+
# heartbeat broadcasts. This method must be called after initialize
|
42
|
+
# or this node will not be a cluster member.
|
43
|
+
def start
|
44
|
+
@connection.connect
|
45
|
+
@publisher.broadcast(:online)
|
46
|
+
@subscriber.subscribe
|
47
|
+
|
48
|
+
EM.add_periodic_timer(1) { heartbeat }
|
49
|
+
|
50
|
+
at_exit do
|
51
|
+
@publisher.broadcast(:offline)
|
52
|
+
@sessions.delete_all(@id)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns any streams hosted at remote nodes for these JIDs. The streams act
|
57
|
+
# like normal EM::Connections, but are actually proxies that route stanzas
|
58
|
+
# over redis pubsub channels to remote nodes.
|
59
|
+
def remote_sessions(*jids)
|
60
|
+
@sessions.find(*jids).map do |session|
|
61
|
+
StreamProxy.new(self, session)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Persist the user's session to the shared redis cache so that other cluster
|
66
|
+
# nodes can locate the node hosting this user's connection and route messages
|
67
|
+
# to them.
|
68
|
+
def save_session(jid, attrs)
|
69
|
+
@sessions.save(jid, attrs)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Remove this user from the cluster routing table so that no further stanzas
|
73
|
+
# may be routed to them. This must be called when the user's session is
|
74
|
+
# terminated, either by logout or stream disconnect.
|
75
|
+
def delete_session(jid)
|
76
|
+
@sessions.delete(jid)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Remove all user sessions from the routing table associated with the
|
80
|
+
# given node ID. Cluster nodes call this themselves during normal shutdown.
|
81
|
+
# However, if a node dies without being properly shutdown, the other nodes
|
82
|
+
# will cleanup its sessions when they detect the node is offline.
|
83
|
+
def delete_sessions(node)
|
84
|
+
@sessions.delete_all(node)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Notify the session store that this node is still alive. The node
|
88
|
+
# broadcasts its current time, so all cluster members' clocks don't
|
89
|
+
# necessarily need to be in sync.
|
90
|
+
def poke(node, time)
|
91
|
+
@sessions.poke(node, time)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Send the stanza to the node hosting the user's session. The stanza is
|
95
|
+
# published to the channel to which the remote node is listening for
|
96
|
+
# messages.
|
97
|
+
def route(stanza, node)
|
98
|
+
@publisher.route(stanza, node)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Notify the remote node that the user's roster has changed and it should
|
102
|
+
# reload the user from storage.
|
103
|
+
def update_user(jid, node)
|
104
|
+
@publisher.update_user(jid, node)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Return the shared redis connection for most queries to use.
|
108
|
+
def connection
|
109
|
+
@connection.connect
|
110
|
+
end
|
111
|
+
|
112
|
+
# Create a new redis connection.
|
113
|
+
def connect
|
114
|
+
@connection.create
|
115
|
+
end
|
116
|
+
|
117
|
+
# Turn an asynchronous redis query into a blocking call by pausing the
|
118
|
+
# fiber in which this code is running. Return the result of the query
|
119
|
+
# from this method, rather than passing it to a callback block.
|
120
|
+
def query(name, *args)
|
121
|
+
fiber, yielding = Fiber.current, true
|
122
|
+
req = connection.send(name, *args)
|
123
|
+
req.errback { fiber.resume rescue yielding = false }
|
124
|
+
req.callback {|response| fiber.resume(response) }
|
125
|
+
Fiber.yield if yielding
|
126
|
+
end
|
127
|
+
|
128
|
+
# Return the connected streams for this user, without any proxy streams
|
129
|
+
# to remote cluster nodes (locally connected streams only).
|
130
|
+
def connected_resources(jid)
|
131
|
+
@config.router.connected_resources(jid, jid, false)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Return the Storage implementation for this domain or nil if the
|
135
|
+
# domain is not hosted here.
|
136
|
+
def storage(domain)
|
137
|
+
@config.storage(domain)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Create a pubsub topic (a.k.a. node), in the given domain, to which
|
141
|
+
# messages may be published. The domain argument will be one of the
|
142
|
+
# configured pubsub subdomains in conf/config.rb (e.g. games.wonderland.lit,
|
143
|
+
# topics.wonderland.lit, etc).
|
144
|
+
def add_pubsub_node(domain, node)
|
145
|
+
@pubsub.add_node(domain, node)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Remove a pubsub topic so messages may no longer be broadcast to it.
|
149
|
+
def delete_pubsub_node(domain, node)
|
150
|
+
@pubsub.delete_node(domain, node)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Subscribe the JID to the pubsub topic so it will receive any messages
|
154
|
+
# published to it.
|
155
|
+
def subscribe_pubsub(domain, node, jid)
|
156
|
+
@pubsub.subscribe(domain, node, jid)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Unsubscribe the JID from the pubsub topic, deregistering its interest
|
160
|
+
# in receiving any messages published to it.
|
161
|
+
def unsubscribe_pubsub(domain, node, jid)
|
162
|
+
@pubsub.unsubscribe(domain, node, jid)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Unsubscribe the JID from all pubsub topics. This is useful when the
|
166
|
+
# JID's session ends by logout or disconnect.
|
167
|
+
def unsubscribe_all_pubsub(domain, jid)
|
168
|
+
@pubsub.unsubscribe_all(domain, jid)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Return true if the pubsub topic exists and messages may be published to it.
|
172
|
+
def pubsub_node?(domain, node)
|
173
|
+
@pubsub.node?(domain, node)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Return true if the JID is a registered subscriber to the pubsub topic and
|
177
|
+
# messages published to it should be routed to the JID.
|
178
|
+
def pubsub_subscribed?(domain, node, jid)
|
179
|
+
@pubsub.subscribed?(domain, node, jid)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Return a list of JIDs subscribed to the pubsub topic.
|
183
|
+
def pubsub_subscribers(domain, node)
|
184
|
+
@pubsub.subscribers(domain, node)
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
# Call this method once per second to broadcast this node's heartbeat and
|
190
|
+
# expire stale user sessions. This method must not raise exceptions or the
|
191
|
+
# timer will stop.
|
192
|
+
def heartbeat
|
193
|
+
@publisher.broadcast(:heartbeat)
|
194
|
+
@sessions.expire
|
195
|
+
rescue => e
|
196
|
+
log.error("Cluster session cleanup failed: #{e}")
|
197
|
+
end
|
198
|
+
|
199
|
+
# StreamProxy behaves like an EM::Connection so that stanzas may be sent to
|
200
|
+
# remote nodes just as they are to locally connected streams. The rest of the
|
201
|
+
# system doesn't know or care that these "streams" send their traffic over
|
202
|
+
# redis pubsub channels.
|
203
|
+
class StreamProxy
|
204
|
+
attr_reader :user
|
205
|
+
|
206
|
+
def initialize(cluster, session)
|
207
|
+
@cluster, @user = cluster, UserProxy.new(cluster, session)
|
208
|
+
@node, @available, @interested, @presence =
|
209
|
+
session.values_at('node', 'available', 'interested', 'presence')
|
210
|
+
|
211
|
+
unless @presence.nil? || @presence.empty?
|
212
|
+
@presence = Nokogiri::XML(@presence).root rescue nil
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def available?
|
217
|
+
@available
|
218
|
+
end
|
219
|
+
|
220
|
+
def interested?
|
221
|
+
@interested
|
222
|
+
end
|
223
|
+
|
224
|
+
def last_broadcast_presence
|
225
|
+
@presence
|
226
|
+
end
|
227
|
+
|
228
|
+
def write(stanza)
|
229
|
+
@cluster.route(stanza, @node)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Proxy User#update_from calls to remote cluster nodes over redis
|
234
|
+
# pubsub channels.
|
235
|
+
class UserProxy < User
|
236
|
+
def initialize(cluster, session)
|
237
|
+
super(jid: session['jid'])
|
238
|
+
@cluster, @node = cluster, session['node']
|
239
|
+
end
|
240
|
+
|
241
|
+
def update_from(user)
|
242
|
+
@cluster.update_user(@jid.bare, @node)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
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
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Command
|
5
|
+
class Init
|
6
|
+
def run(opts)
|
7
|
+
raise 'vines init <domain>' unless opts[:args].size == 1
|
8
|
+
domain = opts[:args].first.downcase
|
9
|
+
dir = File.expand_path(domain)
|
10
|
+
raise "Directory already initialized: #{domain}" if File.exists?(dir)
|
11
|
+
Dir.mkdir(dir)
|
12
|
+
|
13
|
+
create_directories(dir)
|
14
|
+
create_users(domain, dir)
|
15
|
+
update_config(domain, dir)
|
16
|
+
Command::Cert.new.create_cert(domain, File.join(dir, 'conf/certs'))
|
17
|
+
|
18
|
+
puts "Initialized server directory: #{domain}"
|
19
|
+
puts "Run 'cd #{domain} && vines start' to begin"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Limit file system database directory access so the server is the only
|
25
|
+
# process managing the data. The config.rb file contains component and
|
26
|
+
# database passwords, so restrict access to just the server user as well.
|
27
|
+
def create_directories(dir)
|
28
|
+
%w[conf web].each do |sub|
|
29
|
+
FileUtils.cp_r(File.expand_path("../../../../#{sub}", __FILE__), dir)
|
30
|
+
end
|
31
|
+
%w[data log pid].each do |sub|
|
32
|
+
Dir.mkdir(File.join(dir, sub), 0700)
|
33
|
+
end
|
34
|
+
File.chmod(0600, File.join(dir, 'conf/config.rb'))
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_config(domain, dir)
|
38
|
+
config = File.expand_path('conf/config.rb', dir)
|
39
|
+
text = File.read(config, encoding: 'utf-8')
|
40
|
+
File.open(config, 'w:utf-8') do |f|
|
41
|
+
f.write(text.gsub('wonderland.lit', domain))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_users(domain, dir)
|
46
|
+
password = 'secr3t'
|
47
|
+
alice, arthur = %w[alice arthur].map do |jid|
|
48
|
+
User.new(jid: [jid, domain].join('@'),
|
49
|
+
password: BCrypt::Password.create(password).to_s)
|
50
|
+
end
|
51
|
+
|
52
|
+
[[alice, arthur], [arthur, alice]].each do |user, contact|
|
53
|
+
user.roster << Contact.new(
|
54
|
+
jid: contact.jid,
|
55
|
+
name: contact.jid.node.capitalize,
|
56
|
+
subscription: 'both',
|
57
|
+
groups: %w[Buddies])
|
58
|
+
end
|
59
|
+
|
60
|
+
storage = Storage::Local.new { dir(File.join(dir, 'data')) }
|
61
|
+
[alice, arthur].each do |user|
|
62
|
+
storage.save_user(user)
|
63
|
+
puts "Created example user #{user.jid} with password #{password}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Command
|
5
|
+
class Ldap
|
6
|
+
def run(opts)
|
7
|
+
raise 'vines ldap <domain>' unless opts[:args].size == 1
|
8
|
+
require opts[:config]
|
9
|
+
domain = opts[:args].first
|
10
|
+
unless storage = Config.instance.vhost(domain).storage rescue nil
|
11
|
+
raise "#{domain} virtual host not found in conf/config.rb"
|
12
|
+
end
|
13
|
+
unless storage.ldap?
|
14
|
+
raise "LDAP connector not configured for #{domain} virtual host"
|
15
|
+
end
|
16
|
+
$stdout.write('JID: ')
|
17
|
+
jid = $stdin.gets.chomp
|
18
|
+
jid = [jid, domain].join('@') unless jid.include?('@')
|
19
|
+
$stdout.write('Password: ')
|
20
|
+
`stty -echo`
|
21
|
+
password = $stdin.gets.chomp
|
22
|
+
`stty echo`
|
23
|
+
puts
|
24
|
+
|
25
|
+
begin
|
26
|
+
user = storage.ldap.authenticate(jid, password)
|
27
|
+
rescue => e
|
28
|
+
raise "LDAP connection failed: #{e.message}"
|
29
|
+
end
|
30
|
+
|
31
|
+
filter = storage.ldap.filter(jid)
|
32
|
+
raise "User not found with filter:\n #{filter}" unless user
|
33
|
+
name = user.name.empty? ? '<name missing>' : user.name
|
34
|
+
puts "Found user #{name} with filter:\n #{filter}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Command
|
5
|
+
class Schema
|
6
|
+
def run(opts)
|
7
|
+
raise 'vines schema <domain>' unless opts[:args].size == 1
|
8
|
+
require opts[:config]
|
9
|
+
domain = opts[:args].first
|
10
|
+
unless storage = Config.instance.vhost(domain).storage rescue nil
|
11
|
+
raise "#{domain} virtual host not found in conf/config.rb"
|
12
|
+
end
|
13
|
+
unless storage.respond_to?(:create_schema)
|
14
|
+
raise "SQL storage not configured for #{domain} virtual host"
|
15
|
+
end
|
16
|
+
begin
|
17
|
+
storage.create_schema
|
18
|
+
rescue => e
|
19
|
+
raise "Schema creation failed: #{e.message}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Command
|
5
|
+
class Start
|
6
|
+
def run(opts)
|
7
|
+
raise 'vines [--pid FILE] start' unless opts[:args].size == 0
|
8
|
+
require opts[:config]
|
9
|
+
server = XmppServer.new(Config.instance)
|
10
|
+
daemonize(opts) if opts[:daemonize]
|
11
|
+
server.start
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def daemonize(opts)
|
17
|
+
daemon = Daemon.new(:pid => opts[:pid], :stdout => opts[:log],
|
18
|
+
:stderr => opts[:log])
|
19
|
+
if daemon.running?
|
20
|
+
raise "Vines is running as process #{daemon.pid}"
|
21
|
+
else
|
22
|
+
puts "Vines has started"
|
23
|
+
daemon.start
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Command
|
5
|
+
class Stop
|
6
|
+
def run(opts)
|
7
|
+
raise 'vines [--pid FILE] stop' unless opts[:args].size == 0
|
8
|
+
daemon = Daemon.new(:pid => opts[:pid])
|
9
|
+
if daemon.running?
|
10
|
+
daemon.stop
|
11
|
+
puts 'Vines has been shutdown'
|
12
|
+
else
|
13
|
+
puts 'Vines is not running'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Config
|
5
|
+
|
6
|
+
# Provides the DSL methods for the virtual host definitions in the
|
7
|
+
# conf/config.rb file. Host instances can be accessed at runtime through
|
8
|
+
# the +Config#vhosts+ method.
|
9
|
+
class Host
|
10
|
+
attr_reader :pubsubs
|
11
|
+
|
12
|
+
def initialize(config, name, &block)
|
13
|
+
@config, @name = config, name.downcase
|
14
|
+
@storage, @ldap = nil, nil
|
15
|
+
@cross_domain_messages = false
|
16
|
+
@private_storage = false
|
17
|
+
@components, @pubsubs = {}, {}
|
18
|
+
validate_domain(@name)
|
19
|
+
instance_eval(&block)
|
20
|
+
raise "storage required for #{@name}" unless @storage
|
21
|
+
end
|
22
|
+
|
23
|
+
def storage(name=nil, &block)
|
24
|
+
if name
|
25
|
+
raise "one storage mechanism per host allowed" if @storage
|
26
|
+
@storage = Storage.from_name(name, &block)
|
27
|
+
@storage.ldap = @ldap
|
28
|
+
else
|
29
|
+
@storage
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def ldap(host='localhost', port=636, &block)
|
34
|
+
@ldap = Storage::Ldap.new(host, port, &block)
|
35
|
+
@storage.ldap = @ldap if @storage
|
36
|
+
end
|
37
|
+
|
38
|
+
def cross_domain_messages(enabled)
|
39
|
+
@cross_domain_messages = !!enabled
|
40
|
+
end
|
41
|
+
|
42
|
+
def cross_domain_messages?
|
43
|
+
@cross_domain_messages
|
44
|
+
end
|
45
|
+
|
46
|
+
def components(options=nil)
|
47
|
+
return @components unless options
|
48
|
+
|
49
|
+
names = options.keys.map {|domain| "#{domain}.#{@name}".downcase }
|
50
|
+
raise "duplicate component domains not allowed" if dupes?(names, @components.keys)
|
51
|
+
raise "pubsub domains overlap component domains" if dupes?(names, @pubsubs.keys)
|
52
|
+
|
53
|
+
options.each do |domain, password|
|
54
|
+
raise 'component domain required' if (domain || '').to_s.strip.empty?
|
55
|
+
raise 'component password required' if (password || '').strip.empty?
|
56
|
+
name = "#{domain}.#{@name}".downcase
|
57
|
+
raise "components must be one level below their host: #{name}" if domain.to_s.include?('.')
|
58
|
+
validate_domain(name)
|
59
|
+
@components[name] = password
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def component?(domain)
|
64
|
+
!!@components[domain.to_s]
|
65
|
+
end
|
66
|
+
|
67
|
+
def password(domain)
|
68
|
+
@components[domain.to_s]
|
69
|
+
end
|
70
|
+
|
71
|
+
def pubsub(*domains)
|
72
|
+
domains.flatten!
|
73
|
+
raise 'define at least one pubsub domain' if domains.empty?
|
74
|
+
names = domains.map {|domain| "#{domain}.#{@name}".downcase }
|
75
|
+
raise "duplicate pubsub domains not allowed" if dupes?(names, @pubsubs.keys)
|
76
|
+
raise "pubsub domains overlap component domains" if dupes?(names, @components.keys)
|
77
|
+
domains.each do |domain|
|
78
|
+
raise 'pubsub domain required' if (domain || '').to_s.strip.empty?
|
79
|
+
name = "#{domain}.#{@name}".downcase
|
80
|
+
raise "pubsub domains must be one level below their host: #{name}" if domain.to_s.include?('.')
|
81
|
+
validate_domain(name)
|
82
|
+
@pubsubs[name] = PubSub.new(@config, name)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def pubsub?(domain)
|
87
|
+
@pubsubs.key?(domain.to_s)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Unsubscribe this JID from all pubsub topics hosted at this virtual host.
|
91
|
+
# This should be called when the user's session ends via logout or
|
92
|
+
# disconnect.
|
93
|
+
def unsubscribe_pubsub(jid)
|
94
|
+
@pubsubs.values.each do |pubsub|
|
95
|
+
pubsub.unsubscribe_all(jid)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def disco_items
|
100
|
+
[@components.keys, @pubsubs.keys].flatten.sort
|
101
|
+
end
|
102
|
+
|
103
|
+
def private_storage(enabled)
|
104
|
+
@private_storage = !!enabled
|
105
|
+
end
|
106
|
+
|
107
|
+
def private_storage?
|
108
|
+
@private_storage
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# Return true if the arrays contain any duplicate items.
|
114
|
+
def dupes?(a, b)
|
115
|
+
a.uniq.size != a.size || b.uniq.size != b.size || (a & b).any?
|
116
|
+
end
|
117
|
+
|
118
|
+
# Prevent domains in config files that won't form valid JIDs.
|
119
|
+
def validate_domain(name)
|
120
|
+
jid = JID.new(name)
|
121
|
+
raise "incorrect domain: #{name}" if jid.node || jid.resource
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|