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.
Files changed (174) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +19 -0
  4. data/README.md +7 -0
  5. data/Rakefile +23 -0
  6. data/bin/vines +4 -0
  7. data/conf/certs/README +39 -0
  8. data/conf/certs/ca-bundle.crt +3895 -0
  9. data/conf/config.rb +42 -0
  10. data/lib/vines/cli.rb +132 -0
  11. data/lib/vines/cluster/connection.rb +26 -0
  12. data/lib/vines/cluster/publisher.rb +55 -0
  13. data/lib/vines/cluster/pubsub.rb +92 -0
  14. data/lib/vines/cluster/sessions.rb +125 -0
  15. data/lib/vines/cluster/subscriber.rb +108 -0
  16. data/lib/vines/cluster.rb +246 -0
  17. data/lib/vines/command/bcrypt.rb +12 -0
  18. data/lib/vines/command/cert.rb +50 -0
  19. data/lib/vines/command/init.rb +68 -0
  20. data/lib/vines/command/ldap.rb +38 -0
  21. data/lib/vines/command/restart.rb +12 -0
  22. data/lib/vines/command/schema.rb +24 -0
  23. data/lib/vines/command/start.rb +28 -0
  24. data/lib/vines/command/stop.rb +18 -0
  25. data/lib/vines/config/host.rb +125 -0
  26. data/lib/vines/config/port.rb +132 -0
  27. data/lib/vines/config/pubsub.rb +108 -0
  28. data/lib/vines/config.rb +223 -0
  29. data/lib/vines/contact.rb +111 -0
  30. data/lib/vines/daemon.rb +78 -0
  31. data/lib/vines/error.rb +150 -0
  32. data/lib/vines/jid.rb +95 -0
  33. data/lib/vines/kit.rb +23 -0
  34. data/lib/vines/log.rb +24 -0
  35. data/lib/vines/router.rb +179 -0
  36. data/lib/vines/stanza/iq/auth.rb +18 -0
  37. data/lib/vines/stanza/iq/disco_info.rb +45 -0
  38. data/lib/vines/stanza/iq/disco_items.rb +29 -0
  39. data/lib/vines/stanza/iq/error.rb +16 -0
  40. data/lib/vines/stanza/iq/ping.rb +16 -0
  41. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  42. data/lib/vines/stanza/iq/query.rb +10 -0
  43. data/lib/vines/stanza/iq/result.rb +16 -0
  44. data/lib/vines/stanza/iq/roster.rb +140 -0
  45. data/lib/vines/stanza/iq/session.rb +17 -0
  46. data/lib/vines/stanza/iq/vcard.rb +56 -0
  47. data/lib/vines/stanza/iq/version.rb +25 -0
  48. data/lib/vines/stanza/iq.rb +48 -0
  49. data/lib/vines/stanza/message.rb +40 -0
  50. data/lib/vines/stanza/presence/error.rb +23 -0
  51. data/lib/vines/stanza/presence/probe.rb +37 -0
  52. data/lib/vines/stanza/presence/subscribe.rb +42 -0
  53. data/lib/vines/stanza/presence/subscribed.rb +51 -0
  54. data/lib/vines/stanza/presence/unavailable.rb +15 -0
  55. data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
  56. data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
  57. data/lib/vines/stanza/presence.rb +141 -0
  58. data/lib/vines/stanza/pubsub/create.rb +39 -0
  59. data/lib/vines/stanza/pubsub/delete.rb +41 -0
  60. data/lib/vines/stanza/pubsub/publish.rb +66 -0
  61. data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
  62. data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
  63. data/lib/vines/stanza/pubsub.rb +22 -0
  64. data/lib/vines/stanza.rb +175 -0
  65. data/lib/vines/storage/ldap.rb +71 -0
  66. data/lib/vines/storage/local.rb +139 -0
  67. data/lib/vines/storage/null.rb +39 -0
  68. data/lib/vines/storage/sql.rb +138 -0
  69. data/lib/vines/storage.rb +239 -0
  70. data/lib/vines/store.rb +110 -0
  71. data/lib/vines/stream/client/auth.rb +74 -0
  72. data/lib/vines/stream/client/auth_restart.rb +29 -0
  73. data/lib/vines/stream/client/bind.rb +72 -0
  74. data/lib/vines/stream/client/bind_restart.rb +24 -0
  75. data/lib/vines/stream/client/closed.rb +13 -0
  76. data/lib/vines/stream/client/ready.rb +17 -0
  77. data/lib/vines/stream/client/session.rb +210 -0
  78. data/lib/vines/stream/client/start.rb +27 -0
  79. data/lib/vines/stream/client/tls.rb +38 -0
  80. data/lib/vines/stream/client.rb +84 -0
  81. data/lib/vines/stream/component/handshake.rb +26 -0
  82. data/lib/vines/stream/component/ready.rb +23 -0
  83. data/lib/vines/stream/component/start.rb +19 -0
  84. data/lib/vines/stream/component.rb +58 -0
  85. data/lib/vines/stream/http/auth.rb +22 -0
  86. data/lib/vines/stream/http/bind.rb +32 -0
  87. data/lib/vines/stream/http/bind_restart.rb +37 -0
  88. data/lib/vines/stream/http/ready.rb +29 -0
  89. data/lib/vines/stream/http/request.rb +172 -0
  90. data/lib/vines/stream/http/session.rb +120 -0
  91. data/lib/vines/stream/http/sessions.rb +65 -0
  92. data/lib/vines/stream/http/start.rb +23 -0
  93. data/lib/vines/stream/http.rb +157 -0
  94. data/lib/vines/stream/parser.rb +79 -0
  95. data/lib/vines/stream/sasl.rb +128 -0
  96. data/lib/vines/stream/server/auth.rb +13 -0
  97. data/lib/vines/stream/server/auth_restart.rb +13 -0
  98. data/lib/vines/stream/server/final_restart.rb +21 -0
  99. data/lib/vines/stream/server/outbound/auth.rb +31 -0
  100. data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
  101. data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
  102. data/lib/vines/stream/server/outbound/final_features.rb +28 -0
  103. data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
  104. data/lib/vines/stream/server/outbound/start.rb +20 -0
  105. data/lib/vines/stream/server/outbound/tls.rb +30 -0
  106. data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
  107. data/lib/vines/stream/server/ready.rb +24 -0
  108. data/lib/vines/stream/server/start.rb +13 -0
  109. data/lib/vines/stream/server/tls.rb +13 -0
  110. data/lib/vines/stream/server.rb +150 -0
  111. data/lib/vines/stream/state.rb +60 -0
  112. data/lib/vines/stream.rb +247 -0
  113. data/lib/vines/token_bucket.rb +55 -0
  114. data/lib/vines/user.rb +123 -0
  115. data/lib/vines/version.rb +6 -0
  116. data/lib/vines/xmpp_server.rb +25 -0
  117. data/lib/vines.rb +203 -0
  118. data/test/cluster/publisher_test.rb +57 -0
  119. data/test/cluster/sessions_test.rb +47 -0
  120. data/test/cluster/subscriber_test.rb +109 -0
  121. data/test/config/host_test.rb +369 -0
  122. data/test/config/pubsub_test.rb +187 -0
  123. data/test/config_test.rb +732 -0
  124. data/test/contact_test.rb +102 -0
  125. data/test/error_test.rb +58 -0
  126. data/test/ext/nokogiri.rb +14 -0
  127. data/test/jid_test.rb +147 -0
  128. data/test/kit_test.rb +31 -0
  129. data/test/router_test.rb +243 -0
  130. data/test/stanza/iq/disco_info_test.rb +78 -0
  131. data/test/stanza/iq/disco_items_test.rb +49 -0
  132. data/test/stanza/iq/private_storage_test.rb +184 -0
  133. data/test/stanza/iq/roster_test.rb +229 -0
  134. data/test/stanza/iq/session_test.rb +25 -0
  135. data/test/stanza/iq/vcard_test.rb +146 -0
  136. data/test/stanza/iq/version_test.rb +64 -0
  137. data/test/stanza/iq_test.rb +70 -0
  138. data/test/stanza/message_test.rb +126 -0
  139. data/test/stanza/presence/probe_test.rb +50 -0
  140. data/test/stanza/presence/subscribe_test.rb +83 -0
  141. data/test/stanza/pubsub/create_test.rb +116 -0
  142. data/test/stanza/pubsub/delete_test.rb +169 -0
  143. data/test/stanza/pubsub/publish_test.rb +309 -0
  144. data/test/stanza/pubsub/subscribe_test.rb +205 -0
  145. data/test/stanza/pubsub/unsubscribe_test.rb +148 -0
  146. data/test/stanza_test.rb +85 -0
  147. data/test/storage/ldap_test.rb +201 -0
  148. data/test/storage/local_test.rb +59 -0
  149. data/test/storage/mock_redis.rb +97 -0
  150. data/test/storage/null_test.rb +29 -0
  151. data/test/storage/storage_tests.rb +182 -0
  152. data/test/storage_test.rb +85 -0
  153. data/test/store_test.rb +130 -0
  154. data/test/stream/client/auth_test.rb +137 -0
  155. data/test/stream/client/ready_test.rb +47 -0
  156. data/test/stream/client/session_test.rb +27 -0
  157. data/test/stream/component/handshake_test.rb +52 -0
  158. data/test/stream/component/ready_test.rb +103 -0
  159. data/test/stream/component/start_test.rb +39 -0
  160. data/test/stream/http/auth_test.rb +70 -0
  161. data/test/stream/http/ready_test.rb +86 -0
  162. data/test/stream/http/request_test.rb +209 -0
  163. data/test/stream/http/sessions_test.rb +49 -0
  164. data/test/stream/http/start_test.rb +50 -0
  165. data/test/stream/parser_test.rb +122 -0
  166. data/test/stream/sasl_test.rb +195 -0
  167. data/test/stream/server/auth_test.rb +61 -0
  168. data/test/stream/server/outbound/auth_test.rb +75 -0
  169. data/test/stream/server/ready_test.rb +98 -0
  170. data/test/test_helper.rb +42 -0
  171. data/test/token_bucket_test.rb +44 -0
  172. data/test/user_test.rb +96 -0
  173. data/vines.gemspec +30 -0
  174. 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,12 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Command
5
+ class Bcrypt
6
+ def run(opts)
7
+ raise 'vines bcrypt <clear text>' unless opts[:args].size == 1
8
+ puts BCrypt::Password.create(opts[:args].first)
9
+ end
10
+ end
11
+ end
12
+ 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,12 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Command
5
+ class Restart
6
+ def run(opts)
7
+ Stop.new.run(opts)
8
+ Start.new.run(opts)
9
+ end
10
+ end
11
+ end
12
+ 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