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
data/lib/vines/error.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class XmppError < StandardError
|
5
|
+
include Nokogiri::XML
|
6
|
+
|
7
|
+
# Returns the XML element name based on the exception class name.
|
8
|
+
# For example, Vines::BadFormat becomes bad-format.
|
9
|
+
def element_name
|
10
|
+
name = self.class.name.split('::').last
|
11
|
+
name.gsub(/([A-Z])/, '-\1').downcase[1..-1]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class SaslError < XmppError
|
16
|
+
NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-sasl'.freeze
|
17
|
+
|
18
|
+
def initialize(text=nil)
|
19
|
+
@text = text
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_xml
|
23
|
+
doc = Document.new
|
24
|
+
doc.create_element('failure') do |node|
|
25
|
+
node.add_namespace(nil, NAMESPACE)
|
26
|
+
node << doc.create_element(element_name)
|
27
|
+
if @text
|
28
|
+
node << doc.create_element('text') do |text|
|
29
|
+
text['xml:lang'] = 'en'
|
30
|
+
text.content = @text
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end.to_xml(:indent => 0).gsub(/\n/, '')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class StreamError < XmppError
|
38
|
+
NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-streams'.freeze
|
39
|
+
|
40
|
+
def initialize(text=nil)
|
41
|
+
@text = text
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_xml
|
45
|
+
doc = Document.new
|
46
|
+
doc.create_element('stream:error') do |el|
|
47
|
+
el << doc.create_element(element_name, 'xmlns' => NAMESPACE)
|
48
|
+
if @text
|
49
|
+
el << doc.create_element('text', @text, 'xmlns' => NAMESPACE, 'xml:lang' => 'en')
|
50
|
+
end
|
51
|
+
end.to_xml(:indent => 0).gsub(/\n/, '')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class StanzaError < XmppError
|
56
|
+
TYPES = %w[auth cancel continue modify wait].freeze
|
57
|
+
KINDS = %w[message presence iq].freeze
|
58
|
+
NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-stanzas'.freeze
|
59
|
+
|
60
|
+
def initialize(el, type, text=nil)
|
61
|
+
raise "type must be one of: %s" % TYPES.join(', ') unless TYPES.include?(type)
|
62
|
+
raise "stanza must be one of: %s" % KINDS.join(', ') unless KINDS.include?(el.name)
|
63
|
+
@stanza_kind, @type, @text = el.name, type, text
|
64
|
+
@id, @from, @to = %w[id from to].map {|a| el[a] }
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_xml
|
68
|
+
doc = Document.new
|
69
|
+
doc.create_element(@stanza_kind) do |el|
|
70
|
+
el['from'] = @to if @to
|
71
|
+
el['id'] = @id if @id
|
72
|
+
el['to'] = @from if @from
|
73
|
+
el['type'] = 'error'
|
74
|
+
el << doc.create_element('error', 'type' => @type) do |error|
|
75
|
+
error << doc.create_element(element_name, 'xmlns' => NAMESPACE)
|
76
|
+
if @text
|
77
|
+
error << doc.create_element('text', @text, 'xmlns' => NAMESPACE, 'xml:lang' => 'en')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end.to_xml(:indent => 0).gsub(/\n/, '')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
module SaslErrors
|
85
|
+
class Aborted < SaslError; end
|
86
|
+
class AccountDisabled < SaslError; end
|
87
|
+
class CredentialsExpired < SaslError; end
|
88
|
+
class EncryptionRequired < SaslError; end
|
89
|
+
class IncorrectEncoding < SaslError; end
|
90
|
+
class InvalidAuthzid < SaslError; end
|
91
|
+
class InvalidMechanism < SaslError; end
|
92
|
+
class MalformedRequest < SaslError; end
|
93
|
+
class MechanismTooWeak < SaslError; end
|
94
|
+
class NotAuthorized < SaslError; end
|
95
|
+
class TemporaryAuthFailure < SaslError; end
|
96
|
+
end
|
97
|
+
|
98
|
+
module StreamErrors
|
99
|
+
class BadFormat < StreamError; end
|
100
|
+
class BadNamespacePrefix < StreamError; end
|
101
|
+
class Conflict < StreamError; end
|
102
|
+
class ConnectionTimeout < StreamError; end
|
103
|
+
class HostGone < StreamError; end
|
104
|
+
class HostUnknown < StreamError; end
|
105
|
+
class ImproperAddressing < StreamError; end
|
106
|
+
class InternalServerError < StreamError; end
|
107
|
+
class InvalidFrom < StreamError; end
|
108
|
+
class InvalidNamespace < StreamError; end
|
109
|
+
class InvalidXml < StreamError; end
|
110
|
+
class NotAuthorized < StreamError; end
|
111
|
+
class NotWellFormed < StreamError; end
|
112
|
+
class PolicyViolation < StreamError; end
|
113
|
+
class RemoteConnectionFailed < StreamError; end
|
114
|
+
class Reset < StreamError; end
|
115
|
+
class ResourceConstraint < StreamError; end
|
116
|
+
class RestrictedXml < StreamError; end
|
117
|
+
class SeeOtherHost < StreamError; end
|
118
|
+
class SystemShutdown < StreamError; end
|
119
|
+
class UndefinedCondition < StreamError; end
|
120
|
+
class UnsupportedEncoding < StreamError; end
|
121
|
+
class UnsupportedFeature < StreamError; end
|
122
|
+
class UnsupportedStanzaType < StreamError; end
|
123
|
+
class UnsupportedVersion < StreamError; end
|
124
|
+
end
|
125
|
+
|
126
|
+
module StanzaErrors
|
127
|
+
class BadRequest < StanzaError; end
|
128
|
+
class Conflict < StanzaError; end
|
129
|
+
class FeatureNotImplemented < StanzaError; end
|
130
|
+
class Forbidden < StanzaError; end
|
131
|
+
class Gone < StanzaError; end
|
132
|
+
class InternalServerError < StanzaError; end
|
133
|
+
class ItemNotFound < StanzaError; end
|
134
|
+
class JidMalformed < StanzaError; end
|
135
|
+
class NotAcceptable < StanzaError; end
|
136
|
+
class NotAllowed < StanzaError; end
|
137
|
+
class NotAuthorized < StanzaError; end
|
138
|
+
class PolicyViolation < StanzaError; end
|
139
|
+
class RecipientUnavailable < StanzaError; end
|
140
|
+
class Redirect < StanzaError; end
|
141
|
+
class RegistrationRequired < StanzaError; end
|
142
|
+
class RemoteServerNotFound < StanzaError; end
|
143
|
+
class RemoteServerTimeout < StanzaError; end
|
144
|
+
class ResourceConstraint < StanzaError; end
|
145
|
+
class ServiceUnavailable < StanzaError; end
|
146
|
+
class SubscriptionRequired < StanzaError; end
|
147
|
+
class UndefinedCondition < StanzaError; end
|
148
|
+
class UnexpectedRequest < StanzaError; end
|
149
|
+
end
|
150
|
+
end
|
data/lib/vines/jid.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class JID
|
5
|
+
include Comparable
|
6
|
+
|
7
|
+
PATTERN = /\A(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?\Z/.freeze
|
8
|
+
|
9
|
+
# http://tools.ietf.org/html/rfc6122#appendix-A
|
10
|
+
NODE_PREP = /[[:cntrl:] "&'\/:<>@]/.freeze
|
11
|
+
|
12
|
+
# http://tools.ietf.org/html/rfc3454#appendix-C
|
13
|
+
NAME_PREP = /[[:cntrl:] ]/.freeze
|
14
|
+
|
15
|
+
# http://tools.ietf.org/html/rfc6122#appendix-B
|
16
|
+
RESOURCE_PREP = /[[:cntrl:]]/.freeze
|
17
|
+
|
18
|
+
attr_reader :node, :domain, :resource
|
19
|
+
attr_writer :resource
|
20
|
+
|
21
|
+
def self.new(node, domain=nil, resource=nil)
|
22
|
+
node.is_a?(JID) ? node : super
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(node, domain=nil, resource=nil)
|
26
|
+
@node, @domain, @resource = node, domain, resource
|
27
|
+
|
28
|
+
if @domain.nil? && @resource.nil?
|
29
|
+
@node, @domain, @resource = @node.to_s.scan(PATTERN).first
|
30
|
+
end
|
31
|
+
[@node, @domain].each {|part| part.downcase! if part }
|
32
|
+
|
33
|
+
validate
|
34
|
+
end
|
35
|
+
|
36
|
+
# Strip the resource part from this JID and return it as a new
|
37
|
+
# JID object. The new JID contains only the optional node part
|
38
|
+
# and the required domain part from the original. This JID remains
|
39
|
+
# unchanged.
|
40
|
+
def bare
|
41
|
+
JID.new(@node, @domain)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return true if this is a bare JID without a resource part.
|
45
|
+
def bare?
|
46
|
+
@resource.nil?
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return true if this is a domain-only JID without a node or resource part.
|
50
|
+
def domain?
|
51
|
+
!empty? && to_s == @domain
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return true if this JID is equal to the empty string ''. That is, it's
|
55
|
+
# missing the node, domain, and resource parts that form a valid JID. It
|
56
|
+
# makes for easier error handling to be able to create JID objects from
|
57
|
+
# strings and then check if they're empty rather than nil.
|
58
|
+
def empty?
|
59
|
+
to_s == ''
|
60
|
+
end
|
61
|
+
|
62
|
+
def <=>(jid)
|
63
|
+
self.to_s <=> jid.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
def eql?(jid)
|
67
|
+
jid.is_a?(JID) && self == jid
|
68
|
+
end
|
69
|
+
|
70
|
+
def hash
|
71
|
+
self.to_s.hash
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
s = @domain
|
76
|
+
s = "#{@node}@#{s}" if @node
|
77
|
+
s = "#{s}/#{@resource}" if @resource
|
78
|
+
s
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def validate
|
84
|
+
[@node, @domain, @resource].each do |part|
|
85
|
+
raise ArgumentError, 'jid too long' if (part || '').size > 1023
|
86
|
+
end
|
87
|
+
raise ArgumentError, 'empty node' if @node && @node.strip.empty?
|
88
|
+
raise ArgumentError, 'node contains invalid characters' if @node && @node =~ NODE_PREP
|
89
|
+
raise ArgumentError, 'empty resource' if @resource && @resource.strip.empty?
|
90
|
+
raise ArgumentError, 'resource contains invalid characters' if @resource && @resource =~ RESOURCE_PREP
|
91
|
+
raise ArgumentError, 'empty domain' if @domain == '' && (@node || @resource)
|
92
|
+
raise ArgumentError, 'domain contains invalid characters' if @domain && @domain =~ NAME_PREP
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/vines/kit.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
# A module for utility methods with no better home.
|
5
|
+
module Kit
|
6
|
+
# Create a hex-encoded, SHA-512 HMAC of the data, using the secret key.
|
7
|
+
def self.hmac(key, data)
|
8
|
+
digest = OpenSSL::Digest.new("sha512")
|
9
|
+
OpenSSL::HMAC.hexdigest(digest, key, data)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Generates a random uuid per rfc 4122 that's useful for including in
|
13
|
+
# stream, iq, and other xmpp stanzas.
|
14
|
+
def self.uuid
|
15
|
+
SecureRandom.uuid
|
16
|
+
end
|
17
|
+
|
18
|
+
# Generates a random 128 character authentication token.
|
19
|
+
def self.auth_token
|
20
|
+
SecureRandom.hex(64)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/vines/log.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Log
|
5
|
+
@@logger = nil
|
6
|
+
def log
|
7
|
+
unless @@logger
|
8
|
+
@@logger = Logger.new(STDOUT)
|
9
|
+
@@logger.level = Logger::INFO
|
10
|
+
@@logger.progname = 'vines'
|
11
|
+
@@logger.formatter = Class.new(Logger::Formatter) do
|
12
|
+
def initialize
|
13
|
+
@time = "%Y-%m-%dT%H:%M:%SZ".freeze
|
14
|
+
@fmt = "[%s] %5s -- %s: %s\n".freeze
|
15
|
+
end
|
16
|
+
def call(severity, time, program, msg)
|
17
|
+
@fmt % [time.utc.strftime(@time), severity, program, msg2str(msg)]
|
18
|
+
end
|
19
|
+
end.new
|
20
|
+
end
|
21
|
+
@@logger
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/vines/router.rb
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
# The router tracks all stream connections to the server for all clients,
|
5
|
+
# servers, and components. It sends stanzas to the correct stream based on
|
6
|
+
# the 'to' attribute. Router is a singleton, shared by all streams, that must
|
7
|
+
# be accessed with +Config#router+.
|
8
|
+
class Router
|
9
|
+
EMPTY = [].freeze
|
10
|
+
|
11
|
+
STREAM_TYPES = [:client, :server, :component].freeze
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@config = config
|
15
|
+
@clients, @servers, @components = {}, [], []
|
16
|
+
@pending = Hash.new {|h,k| h[k] = [] }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns streams for all connected resources for this JID. A resource is
|
20
|
+
# considered connected after it has completed authentication and resource
|
21
|
+
# binding.
|
22
|
+
def connected_resources(jid, from, proxies=true)
|
23
|
+
jid, from = JID.new(jid), JID.new(from)
|
24
|
+
return [] unless @config.allowed?(jid, from)
|
25
|
+
|
26
|
+
local = @clients[jid.bare] || EMPTY
|
27
|
+
local = local.select {|stream| stream.user.jid == jid } unless jid.bare?
|
28
|
+
remote = proxies ? proxies(jid) : EMPTY
|
29
|
+
[local, remote].flatten
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns streams for all available resources for this JID. A resource is
|
33
|
+
# marked available after it sends initial presence.
|
34
|
+
def available_resources(*jids, from)
|
35
|
+
clients(jids, from) do |stream|
|
36
|
+
stream.available?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns streams for all interested resources for this JID. A resource is
|
41
|
+
# marked interested after it requests the roster.
|
42
|
+
def interested_resources(*jids, from)
|
43
|
+
clients(jids, from) do |stream|
|
44
|
+
stream.interested?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Add the connection to the routing table. The connection must return
|
49
|
+
# :client, :server, or :component from its +stream_type+ method so the
|
50
|
+
# router can properly route stanzas to the stream.
|
51
|
+
def <<(stream)
|
52
|
+
case stream_type(stream)
|
53
|
+
when :client then
|
54
|
+
return unless stream.connected?
|
55
|
+
jid = stream.user.jid.bare
|
56
|
+
@clients[jid] ||= []
|
57
|
+
@clients[jid] << stream
|
58
|
+
when :server then @servers << stream
|
59
|
+
when :component then @components << stream
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Remove the connection from the routing table.
|
64
|
+
def delete(stream)
|
65
|
+
case stream_type(stream)
|
66
|
+
when :client then
|
67
|
+
return unless stream.connected?
|
68
|
+
jid = stream.user.jid.bare
|
69
|
+
streams = @clients[jid] || []
|
70
|
+
streams.delete(stream)
|
71
|
+
@clients.delete(jid) if streams.empty?
|
72
|
+
when :server then @servers.delete(stream)
|
73
|
+
when :component then @components.delete(stream)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Send the stanza to the appropriate remote server-to-server stream
|
78
|
+
# or an external component stream.
|
79
|
+
def route(stanza)
|
80
|
+
to, from = %w[to from].map {|attr| JID.new(stanza[attr]) }
|
81
|
+
return unless @config.allowed?(to, from)
|
82
|
+
key = [to.domain, from.domain]
|
83
|
+
|
84
|
+
if stream = connection_to(to, from)
|
85
|
+
stream.write(stanza)
|
86
|
+
elsif @pending.key?(key)
|
87
|
+
@pending[key] << stanza
|
88
|
+
elsif @config.s2s?(to.domain)
|
89
|
+
@pending[key] << stanza
|
90
|
+
Vines::Stream::Server.start(@config, to.domain, from.domain) do |stream|
|
91
|
+
stream ? send_pending(key, stream) : return_pending(key)
|
92
|
+
@pending.delete(key)
|
93
|
+
end
|
94
|
+
else
|
95
|
+
raise StanzaErrors::RemoteServerNotFound.new(stanza, 'cancel')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the total number of streams connected to the server.
|
100
|
+
def size
|
101
|
+
clients = @clients.values.inject(0) {|sum, arr| sum + arr.size }
|
102
|
+
clients + @servers.size + @components.size
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Write all pending stanzas for this domain to the stream. Called after a
|
108
|
+
# s2s stream has successfully connected and we need to dequeue all stanzas
|
109
|
+
# we received while waiting for the connection to finish.
|
110
|
+
def send_pending(key, stream)
|
111
|
+
@pending[key].each do |stanza|
|
112
|
+
stream.write(stanza)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Return all pending stanzas to their senders as remote-server-not-found
|
117
|
+
# errors. Called after a s2s stream has failed to connect.
|
118
|
+
def return_pending(key)
|
119
|
+
@pending[key].each do |stanza|
|
120
|
+
to, from = JID.new(stanza['to']), JID.new(stanza['from'])
|
121
|
+
xml = StanzaErrors::RemoteServerNotFound.new(stanza, 'cancel').to_xml
|
122
|
+
if @config.component?(from)
|
123
|
+
connection_to(from, to).write(xml) rescue nil
|
124
|
+
else
|
125
|
+
connected_resources(from, to).each {|c| c.write(xml) }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Return the client streams to which the from address is allowed to
|
131
|
+
# contact. Apply the filter block to each stream to narrow the results
|
132
|
+
# before returning the streams.
|
133
|
+
def clients(jids, from, &filter)
|
134
|
+
jids = filter_allowed(jids, from)
|
135
|
+
local = @clients.values_at(*jids).compact.flatten.select(&filter)
|
136
|
+
proxies = proxies(*jids).select(&filter)
|
137
|
+
[local, proxies].flatten
|
138
|
+
end
|
139
|
+
|
140
|
+
# Return the bare JIDs from the list that are allowed to talk to
|
141
|
+
# the +from+ JID.
|
142
|
+
def filter_allowed(jids, from)
|
143
|
+
from = JID.new(from)
|
144
|
+
jids.flatten.map {|jid| JID.new(jid).bare }
|
145
|
+
.select {|jid| @config.allowed?(jid, from) }
|
146
|
+
end
|
147
|
+
|
148
|
+
def proxies(*jids)
|
149
|
+
return EMPTY unless @config.cluster?
|
150
|
+
@config.cluster.remote_sessions(*jids)
|
151
|
+
end
|
152
|
+
|
153
|
+
def connection_to(to, from)
|
154
|
+
component_stream(to) || server_stream(to, from)
|
155
|
+
end
|
156
|
+
|
157
|
+
def component_stream(to)
|
158
|
+
@components.select do |stream|
|
159
|
+
stream.ready? && stream.remote_domain == to.domain
|
160
|
+
end.sample
|
161
|
+
end
|
162
|
+
|
163
|
+
def server_stream(to, from)
|
164
|
+
@servers.select do |stream|
|
165
|
+
stream.ready? &&
|
166
|
+
stream.remote_domain == to.domain &&
|
167
|
+
stream.domain == from.domain
|
168
|
+
end.sample
|
169
|
+
end
|
170
|
+
|
171
|
+
def stream_type(connection)
|
172
|
+
connection.stream_type.tap do |type|
|
173
|
+
unless STREAM_TYPES.include?(type)
|
174
|
+
raise ArgumentError, "unexpected stream type: #{type}"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stanza
|
5
|
+
class Iq
|
6
|
+
class Auth < Query
|
7
|
+
register "/iq[@id and @type='get']/ns:query", 'ns' => NAMESPACES[:non_sasl]
|
8
|
+
|
9
|
+
def process
|
10
|
+
# XEP-0078 says we MUST send a service-unavailable error
|
11
|
+
# here, but Adium 1.4.1 won't login if we do that, so just
|
12
|
+
# swallow this stanza.
|
13
|
+
# raise StanzaErrors::ServiceUnavailable.new(@node, 'cancel')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stanza
|
5
|
+
class Iq
|
6
|
+
class DiscoInfo < Query
|
7
|
+
NS = NAMESPACES[:disco_info]
|
8
|
+
|
9
|
+
register "/iq[@id and @type='get']/ns:query", 'ns' => NS
|
10
|
+
|
11
|
+
def process
|
12
|
+
return if route_iq || !allowed?
|
13
|
+
result = to_result.tap do |el|
|
14
|
+
el << el.document.create_element('query') do |query|
|
15
|
+
query.default_namespace = NS
|
16
|
+
if to_pubsub_domain?
|
17
|
+
identity(query, 'pubsub', 'service')
|
18
|
+
pubsub = [:pubsub_create, :pubsub_delete, :pubsub_instant, :pubsub_item_ids, :pubsub_publish, :pubsub_subscribe]
|
19
|
+
features(query, :disco_info, :ping, :pubsub, *pubsub)
|
20
|
+
else
|
21
|
+
identity(query, 'server', 'im')
|
22
|
+
features = [:disco_info, :disco_items, :ping, :vcard, :version]
|
23
|
+
features << :storage if stream.config.private_storage?(validate_to || stream.domain)
|
24
|
+
features(query, features)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
stream.write(result)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def identity(query, category, type)
|
34
|
+
query << query.document.create_element('identity', 'category' => category, 'type' => type)
|
35
|
+
end
|
36
|
+
|
37
|
+
def features(query, *features)
|
38
|
+
features.flatten.each do |feature|
|
39
|
+
query << query.document.create_element('feature', 'var' => NAMESPACES[feature])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stanza
|
5
|
+
class Iq
|
6
|
+
class DiscoItems < Query
|
7
|
+
NS = NAMESPACES[:disco_items]
|
8
|
+
|
9
|
+
register "/iq[@id and @type='get']/ns:query", 'ns' => NS
|
10
|
+
|
11
|
+
def process
|
12
|
+
return if route_iq || !allowed?
|
13
|
+
result = to_result.tap do |el|
|
14
|
+
el << el.document.create_element('query') do |query|
|
15
|
+
query.default_namespace = NS
|
16
|
+
unless to_pubsub_domain?
|
17
|
+
to = (validate_to || stream.domain).to_s
|
18
|
+
stream.config.vhost(to).disco_items.each do |domain|
|
19
|
+
query << el.document.create_element('item', 'jid' => domain)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
stream.write(result)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stanza
|
5
|
+
class Iq
|
6
|
+
class Ping < Iq
|
7
|
+
register "/iq[@id and @type='get']/ns:ping", 'ns' => NAMESPACES[:ping]
|
8
|
+
|
9
|
+
def process
|
10
|
+
return if route_iq || !allowed?
|
11
|
+
stream.write(to_result)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stanza
|
5
|
+
class Iq
|
6
|
+
# Implements the Private Storage feature defined in XEP-0049. Clients are
|
7
|
+
# allowed to save arbitrary XML documents on the server, identified by
|
8
|
+
# element name and namespace.
|
9
|
+
class PrivateStorage < Query
|
10
|
+
NS = NAMESPACES[:storage]
|
11
|
+
|
12
|
+
register "/iq[@id and (@type='get' or @type='set')]/ns:query", 'ns' => NS
|
13
|
+
|
14
|
+
def process
|
15
|
+
validate_to_address
|
16
|
+
validate_storage_enabled
|
17
|
+
validate_children_size
|
18
|
+
validate_namespaces
|
19
|
+
get? ? retrieve_fragment : update_fragment
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def retrieve_fragment
|
25
|
+
found = storage.find_fragment(stream.user.jid, elements.first.elements.first)
|
26
|
+
raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless found
|
27
|
+
|
28
|
+
result = to_result do |node|
|
29
|
+
node << node.document.create_element('query') do |query|
|
30
|
+
query.default_namespace = NS
|
31
|
+
query << found
|
32
|
+
end
|
33
|
+
end
|
34
|
+
stream.write(result)
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_fragment
|
38
|
+
elements.first.elements.each do |node|
|
39
|
+
storage.save_fragment(stream.user.jid, node)
|
40
|
+
end
|
41
|
+
stream.write(to_result)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def to_result
|
47
|
+
super.tap do |node|
|
48
|
+
node['from'] = stream.user.jid.to_s
|
49
|
+
yield node if block_given?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def validate_children_size
|
54
|
+
size = elements.first.elements.size
|
55
|
+
if (get? && size != 1) || (set? && size == 0)
|
56
|
+
raise StanzaErrors::NotAcceptable.new(self, 'modify')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate_to_address
|
61
|
+
to = validate_to
|
62
|
+
unless to.nil? || to == stream.user.jid.bare
|
63
|
+
raise StanzaErrors::Forbidden.new(self, 'cancel')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def validate_storage_enabled
|
68
|
+
unless stream.config.private_storage?(stream.domain)
|
69
|
+
raise StanzaErrors::ServiceUnavailable.new(self, 'cancel')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def validate_namespaces
|
74
|
+
elements.first.elements.each do |node|
|
75
|
+
if node.namespace.nil? || NAMESPACES.values.include?(node.namespace.href)
|
76
|
+
raise StanzaErrors::NotAcceptable.new(self, 'modify')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|