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,132 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Config
5
+ class Port
6
+ include Vines::Log
7
+
8
+ attr_reader :config, :stream
9
+
10
+ %w[host port].each do |name|
11
+ define_method(name) do
12
+ @settings[name.to_sym]
13
+ end
14
+ end
15
+
16
+ def initialize(config, host, port, &block)
17
+ @config, @settings = config, {}
18
+ instance_eval(&block) if block
19
+ defaults = {:host => host, :port => port,
20
+ :max_resources_per_account => 5, :max_stanza_size => 128 * 1024}
21
+ @settings = defaults.merge(@settings)
22
+ end
23
+
24
+ def max_stanza_size(max=nil)
25
+ if max
26
+ # rfc 6120 section 13.12
27
+ @settings[:max_stanza_size] = [10000, max].max
28
+ else
29
+ @settings[:max_stanza_size]
30
+ end
31
+ end
32
+
33
+ def start
34
+ type = stream.name.split('::').last.downcase
35
+ log.info("Accepting #{type} connections on #{host}:#{port}")
36
+ EventMachine::start_server(host, port, stream, config)
37
+ end
38
+ end
39
+
40
+ class ClientPort < Port
41
+ def initialize(config, host='0.0.0.0', port=5222, &block)
42
+ @stream = Vines::Stream::Client
43
+ super(config, host, port, &block)
44
+ end
45
+
46
+ def max_resources_per_account(max=nil)
47
+ if max
48
+ @settings[:max_resources_per_account] = max
49
+ else
50
+ @settings[:max_resources_per_account]
51
+ end
52
+ end
53
+
54
+ def start
55
+ super
56
+ config.cluster.start if config.cluster?
57
+ end
58
+ end
59
+
60
+ class ServerPort < Port
61
+ def initialize(config, host='0.0.0.0', port=5269, &block)
62
+ @hosts, @stream = [], Vines::Stream::Server
63
+ super(config, host, port, &block)
64
+ end
65
+
66
+ def hosts(*hosts)
67
+ if hosts.any?
68
+ @hosts << hosts
69
+ @hosts.flatten!
70
+ else
71
+ @hosts
72
+ end
73
+ end
74
+ end
75
+
76
+ class HttpPort < Port
77
+ def initialize(config, host='0.0.0.0', port=5280, &block)
78
+ @stream = Vines::Stream::Http
79
+ super(config, host, port, &block)
80
+ defaults = {:root => File.expand_path('web'), :bind => '/xmpp'}
81
+ @settings = defaults.merge(@settings)
82
+ end
83
+
84
+ def max_resources_per_account(max=nil)
85
+ if max
86
+ @settings[:max_resources_per_account] = max
87
+ else
88
+ @settings[:max_resources_per_account]
89
+ end
90
+ end
91
+
92
+ def root(dir=nil)
93
+ if dir
94
+ @settings[:root] = File.expand_path(dir)
95
+ else
96
+ @settings[:root]
97
+ end
98
+ end
99
+
100
+ def bind(url=nil)
101
+ if url
102
+ @settings[:bind] = url
103
+ else
104
+ @settings[:bind]
105
+ end
106
+ end
107
+
108
+ def vroute(id=nil)
109
+ if id
110
+ id = id.to_s.strip
111
+ @settings[:vroute] = id.empty? ? nil : id
112
+ else
113
+ @settings[:vroute]
114
+ end
115
+ end
116
+
117
+ def start
118
+ super
119
+ if config.cluster? && vroute.nil?
120
+ log.warn("vroute sticky session cookie not set")
121
+ end
122
+ end
123
+ end
124
+
125
+ class ComponentPort < Port
126
+ def initialize(config, host='0.0.0.0', port=5347, &block)
127
+ @stream = Vines::Stream::Component
128
+ super(config, host, port, &block)
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,108 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Config
5
+ # Provides the configuration DSL to conf/config.rb for pubsub subdomains and
6
+ # exposes the storage and notification systems that the pubsub stanzas need
7
+ # to process. This class hides the complexity of determining pubsub behavior
8
+ # in a standalone vs. clustered chat server environment from the stanzas.
9
+ class PubSub
10
+ def initialize(config, name)
11
+ @config, @name = config, name
12
+ @nodes = {}
13
+ end
14
+
15
+ def add_node(id)
16
+ if @config.cluster?
17
+ @config.cluster.add_pubsub_node(@name, id)
18
+ else
19
+ @nodes[id] ||= Set.new
20
+ end
21
+ end
22
+
23
+ def delete_node(id)
24
+ if @config.cluster?
25
+ @config.cluster.delete_pubsub_node(@name, id)
26
+ else
27
+ @nodes.delete(id)
28
+ end
29
+ end
30
+
31
+ def subscribe(node, jid)
32
+ return unless node?(node) && @config.allowed?(jid, @name)
33
+ if @config.cluster?
34
+ @config.cluster.subscribe_pubsub(@name, node, jid)
35
+ else
36
+ @nodes[node] << JID.new(jid)
37
+ end
38
+ end
39
+
40
+ def unsubscribe(node, jid)
41
+ return unless node?(node)
42
+ if @config.cluster?
43
+ @config.cluster.unsubscribe_pubsub(@name, node, jid)
44
+ else
45
+ @nodes[node].delete(JID.new(jid))
46
+ delete_node(node) if subscribers(node).empty?
47
+ end
48
+ end
49
+
50
+ def unsubscribe_all(jid)
51
+ if @config.cluster?
52
+ @config.cluster.unsubscribe_all_pubsub(@name, jid)
53
+ else
54
+ @nodes.keys.each do |node|
55
+ unsubscribe(node, jid)
56
+ end
57
+ end
58
+ end
59
+
60
+ def node?(node)
61
+ if @config.cluster?
62
+ @config.cluster.pubsub_node?(@name, node)
63
+ else
64
+ @nodes.key?(node)
65
+ end
66
+ end
67
+
68
+ def subscribed?(node, jid)
69
+ return false unless node?(node)
70
+ if @config.cluster?
71
+ @config.cluster.pubsub_subscribed?(@name, node, jid)
72
+ else
73
+ @nodes[node].include?(JID.new(jid))
74
+ end
75
+ end
76
+
77
+ def publish(node, stanza)
78
+ stanza['id'] = Kit.uuid
79
+ stanza['from'] = @name
80
+
81
+ local, remote = subscribers(node).partition {|jid| @config.local_jid?(jid) }
82
+
83
+ local.flat_map do |jid|
84
+ @config.router.connected_resources(jid, @name)
85
+ end.each do |recipient|
86
+ stanza['to'] = recipient.user.jid.to_s
87
+ recipient.write(stanza)
88
+ end
89
+
90
+ remote.each do |jid|
91
+ el = stanza.clone
92
+ el['to'] = jid.to_s
93
+ @config.router.route(el) rescue nil # ignore RemoteServerNotFound
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def subscribers(node)
100
+ if @config.cluster?
101
+ @config.cluster.pubsub_subscribers(@name, node)
102
+ else
103
+ @nodes[node] || []
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,223 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+
5
+ # A Config object is passed to the stream handlers to give them access
6
+ # to server configuration information like virtual host names, storage
7
+ # systems, etc. This class provides the DSL methods used in the
8
+ # conf/config.rb file.
9
+ class Config
10
+ LOG_LEVELS = %w[debug info warn error fatal].freeze
11
+
12
+ attr_reader :router
13
+
14
+ @@instance = nil
15
+ def self.configure(&block)
16
+ @@instance = self.new(&block)
17
+ end
18
+
19
+ def self.instance
20
+ @@instance
21
+ end
22
+
23
+ def initialize(&block)
24
+ @certs = File.expand_path('conf/certs')
25
+ @vhosts, @ports, @cluster = {}, {}, nil
26
+ @null = Storage::Null.new
27
+ @router = Router.new(self)
28
+ instance_eval(&block)
29
+ raise "must define at least one virtual host" if @vhosts.empty?
30
+ end
31
+
32
+ def certs(dir=nil)
33
+ dir ? @certs = File.expand_path(dir) : @certs
34
+ end
35
+
36
+ def host(*names, &block)
37
+ names = names.flatten.map {|name| name.downcase }
38
+ dupes = names.uniq.size != names.size || (@vhosts.keys & names).any?
39
+ raise "one host definition per domain allowed" if dupes
40
+ names.each do |name|
41
+ if name.eql? "diaspora"
42
+ @vhosts[domain_name] = Host.new(self, domain_name, &block)
43
+ else
44
+ @vhosts[name] = Host.new(self, name, &block)
45
+ end
46
+ end
47
+ end
48
+
49
+ def pepper(pepper=nil)
50
+ pepper ? @pepper = pepper : @pepper = ""
51
+ end
52
+
53
+ def domain_name
54
+ AppConfig.environment.url
55
+ .gsub(/^http(s){0,1}:\/\/|\/$/, '')
56
+ .to_s rescue "localhost"
57
+ end
58
+
59
+ %w[client server http component].each do |name|
60
+ define_method(name) do |*args, &block|
61
+ port = Vines::Config.const_get("#{name.capitalize}Port")
62
+ raise "one #{name} port definition allowed" if @ports[name.to_sym]
63
+ @ports[name.to_sym] = port.new(self, *args) do
64
+ instance_eval(&block) if block
65
+ end
66
+ end
67
+ end
68
+
69
+ def cluster(&block)
70
+ return @cluster unless block
71
+ raise "one cluster definition allowed" if @cluster
72
+ @cluster = Cluster.new(self, &block)
73
+ end
74
+
75
+ def log(level)
76
+ const = Logger.const_get(level.to_s.upcase) rescue nil
77
+ unless LOG_LEVELS.include?(level.to_s) && const
78
+ raise "log level must be one of: #{LOG_LEVELS.join(', ')}"
79
+ end
80
+ Class.new.extend(Vines::Log).log.level = const
81
+ end
82
+
83
+ def ports
84
+ @ports.values
85
+ end
86
+
87
+ # Return true if the domain is virtual hosted by this server.
88
+ def vhost?(domain)
89
+ !!vhost(domain)
90
+ end
91
+
92
+ # Return the Host config object for this domain if it's hosted by this
93
+ # server.
94
+ def vhost(domain)
95
+ @vhosts[domain.to_s]
96
+ end
97
+
98
+ # Returns the storage system for the domain or a Storage::Null instance if
99
+ # the domain is not hosted at this server.
100
+ def storage(domain)
101
+ host = vhost(domain)
102
+ host ? host.storage : @null
103
+ end
104
+
105
+ # Returns the PubSub system for the domain or nil if pubsub is not enabled
106
+ # for this domain.
107
+ def pubsub(domain)
108
+ host = @vhosts.values.find {|host| host.pubsub?(domain) }
109
+ host.pubsubs[domain.to_s] if host
110
+ end
111
+
112
+ # Return true if the domain is a pubsub service hosted at a virtual host
113
+ # at this server.
114
+ def pubsub?(domain)
115
+ @vhosts.values.any? {|host| host.pubsub?(domain) }
116
+ end
117
+
118
+ # Return true if all JIDs belong to components hosted by this server.
119
+ def component?(*jids)
120
+ !jids.flatten.index do |jid|
121
+ !component_password(JID.new(jid).domain)
122
+ end
123
+ end
124
+
125
+ # Return the password for the component or nil if it's not hosted here.
126
+ def component_password(domain)
127
+ host = @vhosts.values.find {|host| host.component?(domain) }
128
+ host.password(domain) if host
129
+ end
130
+
131
+ # Return true if all of the JIDs are hosted by this server.
132
+ def local_jid?(*jids)
133
+ !jids.flatten.index do |jid|
134
+ !vhost?(JID.new(jid).domain)
135
+ end
136
+ end
137
+
138
+ # Return true if private XML fragment storage is enabled for this domain.
139
+ def private_storage?(domain)
140
+ host = vhost(domain)
141
+ host.private_storage? if host
142
+ end
143
+
144
+ # Returns true if server-to-server connections are allowed with the
145
+ # given domain.
146
+ def s2s?(domain)
147
+ @ports[:server] && @ports[:server].hosts.include?(domain.to_s)
148
+ end
149
+
150
+ # Return true if the server is a member of a cluster, serving the same
151
+ # domains from different machines.
152
+ def cluster?
153
+ !!@cluster
154
+ end
155
+
156
+ # Retrieve the Port subclass with this name:
157
+ # [:client, :server, :http, :component]
158
+ def [](name)
159
+ @ports[name] or raise ArgumentError.new("no port named #{name}")
160
+ end
161
+
162
+ # Return true if the two JIDs are allowed to send messages to each other.
163
+ # Both domains must have enabled cross_domain_messages in their config files.
164
+ def allowed?(to, from)
165
+ to, from = JID.new(to), JID.new(from)
166
+ return false if to.empty? || from.empty?
167
+ return true if to.domain == from.domain # same domain always allowed
168
+ return cross_domain?(to, from) if local_jid?(to, from) # both virtual hosted here
169
+ return check_subdomains(to, from) if subdomain?(to, from) # component/pubsub to component/pubsub
170
+ return check_subdomain(to, from) if subdomain?(to) # to component/pubsub
171
+ return check_subdomain(from, to) if subdomain?(from) # from component/pubsub
172
+ return cross_domain?(to) if local_jid?(to) # from is remote
173
+ return cross_domain?(from) if local_jid?(from) # to is remote
174
+ return false
175
+ end
176
+
177
+ private
178
+
179
+ # Return true if all of the JIDs are some kind of subdomain resource hosted
180
+ # here (either a component or a pubsub domain).
181
+ def subdomain?(*jids)
182
+ !jids.flatten.index do |jid|
183
+ !component?(jid) && !pubsub?(jid)
184
+ end
185
+ end
186
+
187
+ # Return true if the third-level subdomain JIDs (components and pubsubs)
188
+ # are allowed to communicate with each other. For example, a
189
+ # tea.wonderland.lit component should be allowed to send messages to
190
+ # pubsub.wonderland.lit because they share the second-level wonderland.lit
191
+ # domain.
192
+ def check_subdomains(to, from)
193
+ sub1, sub2 = strip_domain(to), strip_domain(from)
194
+ (sub1 == sub2) || cross_domain?(sub1, sub2)
195
+ end
196
+
197
+ # Return true if the third-level subdomain JID (component or pubsub) is
198
+ # allowed to communicate with the other JID. For example,
199
+ # pubsub.wonderland.lit should be allowed to send messages to
200
+ # alice@wonderland.lit because they share the second-level wonderland.lit
201
+ # domain.
202
+ def check_subdomain(subdomain, jid)
203
+ comp = strip_domain(subdomain)
204
+ return true if comp.domain == jid.domain
205
+ local_jid?(jid) ? cross_domain?(comp, jid) : cross_domain?(comp)
206
+ end
207
+
208
+ # Return the third-level JID's domain with the first subdomain stripped off
209
+ # to create a second-level domain. For example, alice@tea.wonderland.lit
210
+ # returns wonderland.lit.
211
+ def strip_domain(jid)
212
+ domain = jid.domain.split('.').drop(1).join('.')
213
+ JID.new(domain)
214
+ end
215
+
216
+ # Return true if all JIDs are allowed to exchange cross domain messages.
217
+ def cross_domain?(*jids)
218
+ !jids.flatten.index do |jid|
219
+ !vhost(jid.domain).cross_domain_messages?
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,111 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Contact
5
+ include Comparable
6
+
7
+ attr_accessor :name, :subscription, :ask, :groups
8
+ attr_reader :jid
9
+
10
+ def initialize(args={})
11
+ @jid = JID.new(args[:jid]).bare
12
+ raise ArgumentError, 'invalid jid' if @jid.empty?
13
+ @name = args[:name]
14
+ @subscription = args[:subscription] || 'none'
15
+ @ask = args[:ask]
16
+ @groups = args[:groups] || []
17
+ end
18
+
19
+ def <=>(contact)
20
+ contact.is_a?(Contact) ? self.jid.to_s <=> contact.jid.to_s : nil
21
+ end
22
+
23
+ alias :eql? :==
24
+
25
+ def hash
26
+ jid.to_s.hash
27
+ end
28
+
29
+ def update_from(contact)
30
+ @name = contact.name
31
+ @subscription = contact.subscription
32
+ @ask = contact.ask
33
+ @groups = contact.groups.clone
34
+ end
35
+
36
+ # Returns true if this contact is in a state that allows the user
37
+ # to subscribe to their presence updates.
38
+ def can_subscribe?
39
+ @ask == 'subscribe' && %w[none from].include?(@subscription)
40
+ end
41
+
42
+ def subscribe_to
43
+ @subscription = (@subscription == 'none') ? 'to' : 'both'
44
+ @ask = nil
45
+ end
46
+
47
+ def unsubscribe_to
48
+ @subscription = (@subscription == 'both') ? 'from' : 'none'
49
+ end
50
+
51
+ def subscribe_from
52
+ @subscription = (@subscription == 'none') ? 'from' : 'both'
53
+ @ask = nil
54
+ end
55
+
56
+ def unsubscribe_from
57
+ @subscription = (@subscription == 'both') ? 'to' : 'none'
58
+ end
59
+
60
+ # Returns true if the user is subscribed to this contact's
61
+ # presence updates.
62
+ def subscribed_to?
63
+ %w[to both].include?(@subscription)
64
+ end
65
+
66
+ # Returns true if the user has a presence subscription from
67
+ # this contact. The contact is subscribed to this user's presence.
68
+ def subscribed_from?
69
+ %w[from both].include?(@subscription)
70
+ end
71
+
72
+ # Returns a hash of this contact's attributes suitable for persisting in
73
+ # a document store.
74
+ def to_h
75
+ {
76
+ 'name' => @name,
77
+ 'subscription' => @subscription,
78
+ 'ask' => @ask,
79
+ 'groups' => @groups.sort!
80
+ }
81
+ end
82
+
83
+ # Write an iq stanza to the recipient stream representing this contact's
84
+ # current roster item state.
85
+ def send_roster_push(recipient)
86
+ doc = Nokogiri::XML::Document.new
87
+ node = doc.create_element('iq',
88
+ 'id' => Kit.uuid,
89
+ 'to' => recipient.user.jid.to_s,
90
+ 'type' => 'set')
91
+ node << doc.create_element('query', 'xmlns' => NAMESPACES[:roster]) do |query|
92
+ query << to_roster_xml
93
+ end
94
+ recipient.write(node)
95
+ end
96
+
97
+ # Returns this contact as an xmpp <item> element.
98
+ def to_roster_xml
99
+ doc = Nokogiri::XML::Document.new
100
+ doc.create_element('item') do |el|
101
+ el['ask'] = @ask unless @ask.nil? || @ask.empty?
102
+ el['jid'] = @jid.bare.to_s
103
+ el['name'] = @name unless @name.nil? || @name.empty?
104
+ el['subscription'] = @subscription
105
+ @groups.sort!.each do |group|
106
+ el << doc.create_element('group', group)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,78 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+
5
+ # Fork the current process into the background and manage pid
6
+ # files so we can kill the process later.
7
+ class Daemon
8
+
9
+ # Configure a new daemon process. Arguments hash can include the following
10
+ # keys: :pid (pid file name, required),
11
+ # :stdin, :stdout, :stderr (default to /dev/null)
12
+ def initialize(args)
13
+ @pid = args[:pid]
14
+ raise ArgumentError.new('pid file is required') unless @pid
15
+ raise ArgumentError.new('pid must be a file name') if File.directory?(@pid)
16
+ raise ArgumentError.new('pid file must be writable') unless File.writable?(File.dirname(@pid))
17
+ @stdin, @stdout, @stderr = [:stdin, :stdout, :stderr].map {|k| args[k] || '/dev/null' }
18
+ end
19
+
20
+ # Fork the current process into the background to start the
21
+ # daemon. Do nothing if the daemon is already running.
22
+ def start
23
+ daemonize unless running?
24
+ end
25
+
26
+ # Use the pid stored in the pid file created from a previous
27
+ # call to start to send a TERM signal to the process. Do nothing
28
+ # if the daemon is not running.
29
+ def stop
30
+ 10.times do
31
+ break unless running?
32
+ Process.kill('TERM', pid)
33
+ sleep(0.1)
34
+ end
35
+ end
36
+
37
+ # Returns true if the process is running as determined by the numeric
38
+ # pid stored in the pid file created by a previous call to start.
39
+ def running?
40
+ begin
41
+ pid && Process.kill(0, pid)
42
+ rescue Errno::ESRCH
43
+ delete_pid
44
+ false
45
+ rescue Errno::EPERM
46
+ true
47
+ end
48
+ end
49
+
50
+ # Returns the numeric process ID from the pid file.
51
+ # If the pid file does not exist, returns nil.
52
+ def pid
53
+ File.read(@pid).to_i if File.exists?(@pid)
54
+ end
55
+
56
+ private
57
+
58
+ def delete_pid
59
+ File.delete(@pid) if File.exists?(@pid)
60
+ end
61
+
62
+ # Fork process into background twice to release it from
63
+ # the controlling tty. Point open file descriptors shared
64
+ # with the parent process to separate destinations (e.g. /dev/null).
65
+ def daemonize
66
+ exit if fork
67
+ Process.setsid
68
+ exit if fork
69
+ Dir.chdir('/')
70
+ $stdin.reopen(@stdin)
71
+ $stdout.reopen(@stdout, 'a').sync = true
72
+ $stderr.reopen(@stderr, 'a').sync = true
73
+ File.open(@pid, 'w') {|f| f.write(Process.pid) }
74
+ at_exit { delete_pid }
75
+ trap('TERM') { exit }
76
+ end
77
+ end
78
+ end