lygneo-vines 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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/daemon.rb +78 -0
  30. data/lib/vines/error.rb +150 -0
  31. data/lib/vines/follower.rb +111 -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/error_test.rb +58 -0
  125. data/test/ext/nokogiri.rb +14 -0
  126. data/test/follower_test.rb +102 -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, follower|
53
+ user.roster << Follower.new(
54
+ jid: follower.jid,
55
+ name: follower.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