shingara-blather 0.4.8
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.md +162 -0
- data/examples/echo.rb +18 -0
- data/examples/execute.rb +16 -0
- data/examples/ping_pong.rb +37 -0
- data/examples/print_hierarchy.rb +76 -0
- data/examples/rosterprint.rb +14 -0
- data/examples/stream_only.rb +27 -0
- data/examples/xmpp4r/echo.rb +35 -0
- data/lib/blather/client/client.rb +310 -0
- data/lib/blather/client/dsl/pubsub.rb +170 -0
- data/lib/blather/client/dsl.rb +264 -0
- data/lib/blather/client.rb +87 -0
- data/lib/blather/core_ext/nokogiri.rb +40 -0
- data/lib/blather/errors/sasl_error.rb +43 -0
- data/lib/blather/errors/stanza_error.rb +107 -0
- data/lib/blather/errors/stream_error.rb +82 -0
- data/lib/blather/errors.rb +69 -0
- data/lib/blather/jid.rb +142 -0
- data/lib/blather/roster.rb +111 -0
- data/lib/blather/roster_item.rb +122 -0
- data/lib/blather/stanza/disco/disco_info.rb +176 -0
- data/lib/blather/stanza/disco/disco_items.rb +132 -0
- data/lib/blather/stanza/disco.rb +25 -0
- data/lib/blather/stanza/iq/query.rb +53 -0
- data/lib/blather/stanza/iq/roster.rb +179 -0
- data/lib/blather/stanza/iq.rb +138 -0
- data/lib/blather/stanza/message.rb +332 -0
- data/lib/blather/stanza/presence/status.rb +212 -0
- data/lib/blather/stanza/presence/subscription.rb +101 -0
- data/lib/blather/stanza/presence.rb +163 -0
- data/lib/blather/stanza/pubsub/affiliations.rb +79 -0
- data/lib/blather/stanza/pubsub/create.rb +65 -0
- data/lib/blather/stanza/pubsub/errors.rb +18 -0
- data/lib/blather/stanza/pubsub/event.rb +123 -0
- data/lib/blather/stanza/pubsub/items.rb +103 -0
- data/lib/blather/stanza/pubsub/publish.rb +103 -0
- data/lib/blather/stanza/pubsub/retract.rb +92 -0
- data/lib/blather/stanza/pubsub/subscribe.rb +68 -0
- data/lib/blather/stanza/pubsub/subscription.rb +134 -0
- data/lib/blather/stanza/pubsub/subscriptions.rb +81 -0
- data/lib/blather/stanza/pubsub/unsubscribe.rb +68 -0
- data/lib/blather/stanza/pubsub.rb +129 -0
- data/lib/blather/stanza/pubsub_owner/delete.rb +52 -0
- data/lib/blather/stanza/pubsub_owner/purge.rb +52 -0
- data/lib/blather/stanza/pubsub_owner.rb +51 -0
- data/lib/blather/stanza.rb +149 -0
- data/lib/blather/stream/client.rb +31 -0
- data/lib/blather/stream/component.rb +38 -0
- data/lib/blather/stream/features/resource.rb +63 -0
- data/lib/blather/stream/features/sasl.rb +187 -0
- data/lib/blather/stream/features/session.rb +44 -0
- data/lib/blather/stream/features/tls.rb +28 -0
- data/lib/blather/stream/features.rb +53 -0
- data/lib/blather/stream/parser.rb +102 -0
- data/lib/blather/stream.rb +231 -0
- data/lib/blather/xmpp_node.rb +218 -0
- data/lib/blather.rb +78 -0
- data/spec/blather/client/client_spec.rb +559 -0
- data/spec/blather/client/dsl/pubsub_spec.rb +462 -0
- data/spec/blather/client/dsl_spec.rb +143 -0
- data/spec/blather/core_ext/nokogiri_spec.rb +83 -0
- data/spec/blather/errors/sasl_error_spec.rb +33 -0
- data/spec/blather/errors/stanza_error_spec.rb +129 -0
- data/spec/blather/errors/stream_error_spec.rb +108 -0
- data/spec/blather/errors_spec.rb +33 -0
- data/spec/blather/jid_spec.rb +87 -0
- data/spec/blather/roster_item_spec.rb +96 -0
- data/spec/blather/roster_spec.rb +103 -0
- data/spec/blather/stanza/discos/disco_info_spec.rb +226 -0
- data/spec/blather/stanza/discos/disco_items_spec.rb +148 -0
- data/spec/blather/stanza/iq/query_spec.rb +64 -0
- data/spec/blather/stanza/iq/roster_spec.rb +140 -0
- data/spec/blather/stanza/iq_spec.rb +45 -0
- data/spec/blather/stanza/message_spec.rb +132 -0
- data/spec/blather/stanza/presence/status_spec.rb +132 -0
- data/spec/blather/stanza/presence/subscription_spec.rb +105 -0
- data/spec/blather/stanza/presence_spec.rb +66 -0
- data/spec/blather/stanza/pubsub/affiliations_spec.rb +57 -0
- data/spec/blather/stanza/pubsub/create_spec.rb +56 -0
- data/spec/blather/stanza/pubsub/event_spec.rb +84 -0
- data/spec/blather/stanza/pubsub/items_spec.rb +79 -0
- data/spec/blather/stanza/pubsub/publish_spec.rb +83 -0
- data/spec/blather/stanza/pubsub/retract_spec.rb +75 -0
- data/spec/blather/stanza/pubsub/subscribe_spec.rb +61 -0
- data/spec/blather/stanza/pubsub/subscription_spec.rb +97 -0
- data/spec/blather/stanza/pubsub/subscriptions_spec.rb +59 -0
- data/spec/blather/stanza/pubsub/unsubscribe_spec.rb +61 -0
- data/spec/blather/stanza/pubsub_owner/delete_spec.rb +50 -0
- data/spec/blather/stanza/pubsub_owner/purge_spec.rb +50 -0
- data/spec/blather/stanza/pubsub_owner_spec.rb +27 -0
- data/spec/blather/stanza/pubsub_spec.rb +67 -0
- data/spec/blather/stanza_spec.rb +116 -0
- data/spec/blather/stream/client_spec.rb +1011 -0
- data/spec/blather/stream/component_spec.rb +95 -0
- data/spec/blather/stream/parser_spec.rb +145 -0
- data/spec/blather/xmpp_node_spec.rb +231 -0
- data/spec/fixtures/pubsub.rb +311 -0
- data/spec/spec_helper.rb +43 -0
- metadata +249 -0
@@ -0,0 +1,187 @@
|
|
1
|
+
module Blather # :nodoc:
|
2
|
+
class Stream # :nodoc:
|
3
|
+
|
4
|
+
class SASL < Features # :nodoc:
|
5
|
+
class UnknownMechanism < BlatherError
|
6
|
+
register :sasl_unknown_mechanism
|
7
|
+
end
|
8
|
+
|
9
|
+
MECHANISMS = %w[
|
10
|
+
digest-md5
|
11
|
+
plain
|
12
|
+
anonymous
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
SASL_NS = 'urn:ietf:params:xml:ns:xmpp-sasl'.freeze
|
16
|
+
register SASL_NS
|
17
|
+
|
18
|
+
def initialize(stream, succeed, fail)
|
19
|
+
super
|
20
|
+
@jid = @stream.jid
|
21
|
+
@pass = @stream.password
|
22
|
+
@mechanisms = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def receive_data(stanza)
|
26
|
+
@node = stanza
|
27
|
+
case stanza.element_name
|
28
|
+
when 'mechanisms'
|
29
|
+
available_mechanisms = stanza.children.map { |m| m.content.downcase }
|
30
|
+
@mechanisms = MECHANISMS.select { |m| available_mechanisms.include? m }
|
31
|
+
next!
|
32
|
+
when 'failure'
|
33
|
+
next!
|
34
|
+
when 'success'
|
35
|
+
@stream.start
|
36
|
+
else
|
37
|
+
if self.respond_to?(stanza.element_name)
|
38
|
+
self.__send__(stanza.element_name)
|
39
|
+
else
|
40
|
+
fail! UnknownResponse.new(stanza)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
def next!
|
47
|
+
if @jid.node == ''
|
48
|
+
process_anonymous
|
49
|
+
else
|
50
|
+
@idx = @idx ? @idx+1 : 0
|
51
|
+
authenticate_with @mechanisms[@idx]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_anonymous
|
56
|
+
if @mechanisms.include?('anonymous')
|
57
|
+
authenticate_with 'anonymous'
|
58
|
+
else
|
59
|
+
fail! BlatherError.new('The server does not support ANONYMOUS login. You must provide a node in the JID')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def authenticate_with(method)
|
64
|
+
method = case method
|
65
|
+
when 'digest-md5' then DigestMD5
|
66
|
+
when 'plain' then Plain
|
67
|
+
when 'anonymous' then Anonymous
|
68
|
+
when nil then fail!(SASLError.import(@node))
|
69
|
+
else next!
|
70
|
+
end
|
71
|
+
|
72
|
+
if method.is_a?(Module)
|
73
|
+
extend method
|
74
|
+
authenticate
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Base64 Encoder
|
80
|
+
def b64(str)
|
81
|
+
[str].pack('m').gsub(/\s/,'')
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Builds a standard auth node
|
86
|
+
def auth_node(mechanism, content = nil)
|
87
|
+
node = XMPPNode.new 'auth'
|
88
|
+
node.content = content if content
|
89
|
+
node.namespace = SASL_NS
|
90
|
+
node[:mechanism] = mechanism
|
91
|
+
node
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Digest MD5 authentication
|
96
|
+
module DigestMD5 # :nodoc:
|
97
|
+
##
|
98
|
+
# Lets the server know we're going to try DigestMD5 authentication
|
99
|
+
def authenticate
|
100
|
+
@stream.send auth_node('DIGEST-MD5')
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# Receive the challenge command.
|
105
|
+
def challenge
|
106
|
+
decode_challenge
|
107
|
+
respond
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
##
|
112
|
+
# Decodes digest strings 'foo=bar,baz="faz"'
|
113
|
+
# into {'foo' => 'bar', 'baz' => 'faz'}
|
114
|
+
def decode_challenge
|
115
|
+
text = @node.content.unpack('m').first
|
116
|
+
res = {}
|
117
|
+
|
118
|
+
text.split(',').each do |statement|
|
119
|
+
key, value = statement.split('=')
|
120
|
+
res[key] = value.delete('"') unless key.empty?
|
121
|
+
end
|
122
|
+
Blather.logger.debug "CHALLENGE DECODE: #{res.inspect}"
|
123
|
+
|
124
|
+
@nonce ||= res['nonce']
|
125
|
+
@realm ||= res['realm']
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# Builds the properly encoded challenge response
|
130
|
+
def generate_response
|
131
|
+
a1 = "#{d("#{@response[:username]}:#{@response[:realm]}:#{@pass}")}:#{@response[:nonce]}:#{@response[:cnonce]}"
|
132
|
+
a2 = "AUTHENTICATE:#{@response[:'digest-uri']}"
|
133
|
+
h("#{h(a1)}:#{@response[:nonce]}:#{@response[:nc]}:#{@response[:cnonce]}:#{@response[:qop]}:#{h(a2)}")
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# Send challenge response
|
138
|
+
def respond
|
139
|
+
node = XMPPNode.new 'response'
|
140
|
+
node.namespace = SASL_NS
|
141
|
+
|
142
|
+
unless @initial_response_sent
|
143
|
+
@initial_response_sent = true
|
144
|
+
@response = {
|
145
|
+
:nonce => @nonce,
|
146
|
+
:charset => 'utf-8',
|
147
|
+
:username => @jid.node,
|
148
|
+
:realm => @realm || @jid.domain,
|
149
|
+
:cnonce => h(Time.new.to_f.to_s),
|
150
|
+
:nc => '00000001',
|
151
|
+
:qop => 'auth',
|
152
|
+
:'digest-uri' => "xmpp/#{@jid.domain}",
|
153
|
+
}
|
154
|
+
@response[:response] = generate_response
|
155
|
+
@response.each { |k,v| @response[k] = "\"#{v}\"" unless [:nc, :qop, :response, :charset].include?(k) }
|
156
|
+
|
157
|
+
Blather.logger.debug "CHALLENGE RESPONSE: #{@response.inspect}"
|
158
|
+
Blather.logger.debug "CH RESP TXT: #{@response.map { |k,v| "#{k}=#{v}" } * ','}"
|
159
|
+
|
160
|
+
# order is to simplify testing
|
161
|
+
# Ruby 1.9 eliminates the need for this with ordered hashes
|
162
|
+
order = [:nonce, :charset, :username, :realm, :cnonce, :nc, :qop, :'digest-uri', :response]
|
163
|
+
node.content = b64(order.map { |k| v = @response[k]; "#{k}=#{v}" } * ',')
|
164
|
+
end
|
165
|
+
|
166
|
+
@stream.send node
|
167
|
+
end
|
168
|
+
|
169
|
+
def d(s); Digest::MD5.digest(s); end
|
170
|
+
def h(s); Digest::MD5.hexdigest(s); end
|
171
|
+
end #DigestMD5
|
172
|
+
|
173
|
+
module Plain # :nodoc:
|
174
|
+
def authenticate
|
175
|
+
@stream.send auth_node('PLAIN', b64("#{@jid.stripped}\x00#{@jid.node}\x00#{@pass}"))
|
176
|
+
end
|
177
|
+
end #Plain
|
178
|
+
|
179
|
+
module Anonymous # :nodoc:
|
180
|
+
def authenticate
|
181
|
+
@stream.send auth_node('ANONYMOUS')
|
182
|
+
end
|
183
|
+
end #Anonymous
|
184
|
+
end #SASL
|
185
|
+
|
186
|
+
end #Stream
|
187
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Blather # :nodoc:
|
2
|
+
class Stream # :nodoc:
|
3
|
+
|
4
|
+
class Session < Features # :nodoc:
|
5
|
+
SESSION_NS = 'urn:ietf:params:xml:ns:xmpp-session'.freeze
|
6
|
+
register SESSION_NS
|
7
|
+
|
8
|
+
def initialize(stream, succeed, fail)
|
9
|
+
super
|
10
|
+
@to = @stream.jid.domain
|
11
|
+
end
|
12
|
+
|
13
|
+
def receive_data(stanza)
|
14
|
+
@node = stanza
|
15
|
+
case stanza.element_name
|
16
|
+
when 'session' then session
|
17
|
+
when 'iq' then check_response
|
18
|
+
else fail!(UnknownResponse.new(stanza))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def check_response
|
24
|
+
if @node[:type] == 'result'
|
25
|
+
succeed!
|
26
|
+
else
|
27
|
+
fail!(StanzaError.import(@node))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Send a start session command
|
33
|
+
def session
|
34
|
+
response = Stanza::Iq.new :set
|
35
|
+
response.to = @to
|
36
|
+
response << (sess = XMPPNode.new('session', response.document))
|
37
|
+
sess.namespace = SESSION_NS
|
38
|
+
|
39
|
+
@stream.send response
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Blather # :nodoc:
|
2
|
+
class Stream # :nodoc:
|
3
|
+
|
4
|
+
class TLS < Features # :nodoc:
|
5
|
+
class TLSFailure < BlatherError
|
6
|
+
register :tls_failure
|
7
|
+
end
|
8
|
+
|
9
|
+
TLS_NS = 'urn:ietf:params:xml:ns:xmpp-tls'.freeze
|
10
|
+
register TLS_NS
|
11
|
+
|
12
|
+
def receive_data(stanza)
|
13
|
+
case stanza.element_name
|
14
|
+
when 'starttls'
|
15
|
+
@stream.send "<starttls xmlns='#{TLS_NS}'/>"
|
16
|
+
when 'proceed'
|
17
|
+
@stream.start_tls
|
18
|
+
@stream.start
|
19
|
+
# succeed!
|
20
|
+
else
|
21
|
+
fail! TLSFailure.new
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end #TLS
|
26
|
+
|
27
|
+
end #Stream
|
28
|
+
end #Blather
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Blather # :nodoc:
|
2
|
+
class Stream # :nodoc:
|
3
|
+
|
4
|
+
class Features # :nodoc:
|
5
|
+
@@features = {}
|
6
|
+
def self.register(ns)
|
7
|
+
@@features[ns] = self
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.from_namespace(ns)
|
11
|
+
@@features[ns]
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(stream, succeed, fail)
|
15
|
+
@stream = stream
|
16
|
+
@succeed = succeed
|
17
|
+
@fail = fail
|
18
|
+
end
|
19
|
+
|
20
|
+
def receive_data(stanza)
|
21
|
+
if @feature
|
22
|
+
@feature.receive_data stanza
|
23
|
+
else
|
24
|
+
@features ||= stanza
|
25
|
+
next!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def next!
|
30
|
+
@idx = @idx ? @idx+1 : 0
|
31
|
+
if stanza = @features.children[@idx]
|
32
|
+
if stanza.namespaces['xmlns'] && (klass = self.class.from_namespace(stanza.namespaces['xmlns']))
|
33
|
+
@feature = klass.new @stream, proc { next! }, @fail
|
34
|
+
@feature.receive_data stanza
|
35
|
+
else
|
36
|
+
next!
|
37
|
+
end
|
38
|
+
else
|
39
|
+
succeed!
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def succeed!
|
44
|
+
@succeed.call
|
45
|
+
end
|
46
|
+
|
47
|
+
def fail!(msg)
|
48
|
+
@fail.call msg
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end #Stream
|
53
|
+
end #Blather
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module Blather # :nodoc:
|
4
|
+
class Stream # :nodoc:
|
5
|
+
|
6
|
+
class Parser < Nokogiri::XML::SAX::Document # :nodoc:
|
7
|
+
NS_TO_IGNORE = %w[jabber:client jabber:component:accept]
|
8
|
+
|
9
|
+
@@debug = false
|
10
|
+
def self.debug; @@debug; end
|
11
|
+
def self.debug=(debug); @@debug = debug; end
|
12
|
+
|
13
|
+
def initialize(receiver)
|
14
|
+
@receiver = receiver
|
15
|
+
@current = nil
|
16
|
+
@namespaces = {}
|
17
|
+
@namespace_definitions = []
|
18
|
+
@parser = Nokogiri::XML::SAX::PushParser.new self
|
19
|
+
end
|
20
|
+
|
21
|
+
def receive_data(string)
|
22
|
+
Blather.logger.debug "PARSING: (#{string})" if @@debug
|
23
|
+
@parser << string
|
24
|
+
self
|
25
|
+
end
|
26
|
+
alias_method :<<, :receive_data
|
27
|
+
|
28
|
+
def start_element_namespace(elem, attrs, prefix, uri, namespaces)
|
29
|
+
Blather.logger.debug "START ELEM: (#{{:elem => elem, :attrs => attrs, :prefix => prefix, :uri => uri, :ns => namespaces}.inspect})" if @@debug
|
30
|
+
|
31
|
+
args = [elem]
|
32
|
+
args << @current.document if @current
|
33
|
+
node = XMPPNode.new *args
|
34
|
+
node.document.root = node unless @current
|
35
|
+
|
36
|
+
attrs.each do |attr|
|
37
|
+
node[attr.localname] = attr.value
|
38
|
+
end
|
39
|
+
|
40
|
+
if !@receiver.stopped?
|
41
|
+
@current << node if @current
|
42
|
+
@current = node
|
43
|
+
end
|
44
|
+
|
45
|
+
ns_keys = namespaces.map { |pre, href| pre }
|
46
|
+
namespaces.delete_if { |pre, href| NS_TO_IGNORE.include? href }
|
47
|
+
@namespace_definitions.push []
|
48
|
+
namespaces.each do |pre, href|
|
49
|
+
next if @namespace_definitions.flatten.include?(@namespaces[[pre, href]])
|
50
|
+
ns = node.add_namespace(pre, href)
|
51
|
+
@namespaces[[pre, href]] ||= ns
|
52
|
+
end
|
53
|
+
@namespaces[[prefix, uri]] ||= node.add_namespace(prefix, uri) if prefix && !ns_keys.include?(prefix)
|
54
|
+
node.namespace = @namespaces[[prefix, uri]]
|
55
|
+
|
56
|
+
deliver(node) if elem == 'stream'
|
57
|
+
|
58
|
+
# $stderr.puts "\n\n"
|
59
|
+
# $stderr.puts [elem, attrs, prefix, uri, namespaces].inspect
|
60
|
+
# $stderr.puts @namespaces.inspect
|
61
|
+
# $stderr.puts [@namespaces[[prefix, uri]].prefix, @namespaces[[prefix, uri]].href].inspect if @namespaces[[prefix, uri]]
|
62
|
+
# $stderr.puts node.inspect
|
63
|
+
# $stderr.puts node.document.to_s.gsub(/\n\s*/,'')
|
64
|
+
end
|
65
|
+
|
66
|
+
def end_element_namespace(elem, prefix, uri)
|
67
|
+
Blather.logger.debug "END ELEM: #{{:elem => elem, :prefix => prefix, :uri => uri}.inspect}" if @@debug
|
68
|
+
|
69
|
+
if elem == 'stream'
|
70
|
+
node = XMPPNode.new('end')
|
71
|
+
node.namespace = {prefix => uri}
|
72
|
+
deliver node
|
73
|
+
elsif @current.parent != @current.document
|
74
|
+
@namespace_definitions.pop
|
75
|
+
@current = @current.parent
|
76
|
+
else
|
77
|
+
deliver @current
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def characters(chars = '')
|
82
|
+
Blather.logger.debug "CHARS: #{chars}" if @@debug
|
83
|
+
@current << Nokogiri::XML::Text.new(chars, @current.document) if @current
|
84
|
+
end
|
85
|
+
|
86
|
+
def warning(msg)
|
87
|
+
Blather.logger.debug "PARSE WARNING: #{msg}" if @@debug
|
88
|
+
end
|
89
|
+
|
90
|
+
def error(msg)
|
91
|
+
raise ParseError.new(msg)
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
def deliver(node)
|
96
|
+
@current, @namespaces, @namespace_definitions = nil, {}, []
|
97
|
+
@receiver.receive node
|
98
|
+
end
|
99
|
+
end #Parser
|
100
|
+
|
101
|
+
end #Stream
|
102
|
+
end #Blather
|
@@ -0,0 +1,231 @@
|
|
1
|
+
module Blather
|
2
|
+
|
3
|
+
# # A pure XMPP stream.
|
4
|
+
#
|
5
|
+
# Blather::Stream can be used to build your own handler system if Blather's
|
6
|
+
# doesn't suit your needs. It will take care of the entire connection
|
7
|
+
# process then start sending Stanza objects back to the registered client.
|
8
|
+
#
|
9
|
+
# The client you register with Blather::Stream needs to implement the following
|
10
|
+
# methods:
|
11
|
+
# * #post_init(stream, jid = nil)
|
12
|
+
# Called after the stream has been initiated.
|
13
|
+
# @param [Blather::Stream] stream is the connected stream object
|
14
|
+
# @param [Blather::JID, nil] jid is the full JID as recognized by the server
|
15
|
+
#
|
16
|
+
# * #receive_data(stanza)
|
17
|
+
# Called every time the stream receives a new stanza
|
18
|
+
# @param [Blather::Stanza] stanza a stanza object from the server
|
19
|
+
#
|
20
|
+
# * #unbind
|
21
|
+
# Called when the stream is shutdown. This will be called regardless of which
|
22
|
+
# side shut the stream down.
|
23
|
+
#
|
24
|
+
# @example Create a new stream and handle it with our own class
|
25
|
+
# class MyClient
|
26
|
+
# attr :jid
|
27
|
+
#
|
28
|
+
# def post_init(stream, jid = nil)
|
29
|
+
# @stream = stream
|
30
|
+
# self.jid = jid
|
31
|
+
# p "Stream Started"
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # Pretty print the stream
|
35
|
+
# def receive_data(stanza)
|
36
|
+
# pp stanza
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# def unbind
|
40
|
+
# p "Stream Ended"
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# def write(what)
|
44
|
+
# @stream.write what
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# client = Blather::Stream.start MyClient.new, "jid@domain/res", "pass"
|
49
|
+
# client.write "[pure xml over the wire]"
|
50
|
+
class Stream < EventMachine::Connection
|
51
|
+
class NoConnection < RuntimeError; end
|
52
|
+
|
53
|
+
STREAM_NS = 'http://etherx.jabber.org/streams'
|
54
|
+
attr_accessor :password
|
55
|
+
attr_reader :jid
|
56
|
+
|
57
|
+
# Start the stream between client and server
|
58
|
+
#
|
59
|
+
# @param [Object] client an object that will respond to #post_init,
|
60
|
+
# #unbind #receive_data
|
61
|
+
# @param [Blather::JID, #to_s] jid the jid to authenticate with
|
62
|
+
# @param [String] pass the password to authenticate with
|
63
|
+
# @param [String, nil] host the hostname or IP to connect to. Default is
|
64
|
+
# to use the domain on the JID
|
65
|
+
# @param [Fixnum, nil] port the port to connect on. Default is the XMPP
|
66
|
+
# default of 5222
|
67
|
+
def self.start(client, jid, pass, host = nil, port = 5222)
|
68
|
+
jid = JID.new jid
|
69
|
+
if host
|
70
|
+
connect host, port, self, client, jid, pass
|
71
|
+
else
|
72
|
+
require 'resolv'
|
73
|
+
srv = []
|
74
|
+
Resolv::DNS.open do |dns|
|
75
|
+
srv = dns.getresources(
|
76
|
+
"_xmpp-client._tcp.#{jid.domain}",
|
77
|
+
Resolv::DNS::Resource::IN::SRV
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
if srv.empty?
|
82
|
+
connect jid.domain, port, self, client, jid, pass
|
83
|
+
else
|
84
|
+
srv.sort! do |a,b|
|
85
|
+
(a.priority != b.priority) ? (a.priority <=> b.priority) :
|
86
|
+
(b.weight <=> a.weight)
|
87
|
+
end
|
88
|
+
|
89
|
+
srv.detect do |r|
|
90
|
+
not connect(r.target.to_s, r.port, self, client, jid, pass) === false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Attempt a connection
|
97
|
+
# Stream will raise +NoConnection+ if it receives #unbind before #post_init
|
98
|
+
# this catches that and returns false prompting for another attempt
|
99
|
+
# @private
|
100
|
+
def self.connect(host, port, conn, client, jid, pass)
|
101
|
+
EM.connect host, port, conn, client, jid, pass
|
102
|
+
rescue NoConnection
|
103
|
+
false
|
104
|
+
end
|
105
|
+
|
106
|
+
[:started, :stopped, :ready, :negotiating].each do |state|
|
107
|
+
define_method("#{state}?") { @state == state }
|
108
|
+
end
|
109
|
+
|
110
|
+
# Send data over the wire
|
111
|
+
#
|
112
|
+
# @todo Queue if not ready
|
113
|
+
#
|
114
|
+
# @param [#to_xml, #to_s] stanza the stanza to send over the wire
|
115
|
+
def send(stanza)
|
116
|
+
Blather.logger.debug "SENDING: (#{caller[1]}) #{stanza}"
|
117
|
+
send_data stanza.respond_to?(:to_xml) ? stanza.to_xml : stanza.to_s
|
118
|
+
end
|
119
|
+
|
120
|
+
# Called by EM.connect to initialize stream variables
|
121
|
+
# @private
|
122
|
+
def initialize(client, jid, pass)
|
123
|
+
super()
|
124
|
+
|
125
|
+
@error = nil
|
126
|
+
@receiver = @client = client
|
127
|
+
|
128
|
+
self.jid = jid
|
129
|
+
@to = self.jid.domain
|
130
|
+
@password = pass
|
131
|
+
end
|
132
|
+
|
133
|
+
# Called when EM completes the connection to the server
|
134
|
+
# this kicks off the starttls/authorize/bind process
|
135
|
+
# @private
|
136
|
+
def connection_completed
|
137
|
+
# @keepalive = EM::PeriodicTimer.new(60) { send_data ' ' }
|
138
|
+
start
|
139
|
+
end
|
140
|
+
|
141
|
+
# Called by EM with data from the wire
|
142
|
+
# @private
|
143
|
+
def receive_data(data)
|
144
|
+
Blather.logger.debug "\n#{'-'*30}\n"
|
145
|
+
Blather.logger.debug "STREAM IN: #{data}"
|
146
|
+
@parser << data
|
147
|
+
|
148
|
+
rescue ParseError => e
|
149
|
+
@error = e
|
150
|
+
send "<stream:error><xml-not-well-formed xmlns='#{StreamError::STREAM_ERR_NS}'/></stream:error>"
|
151
|
+
stop
|
152
|
+
end
|
153
|
+
|
154
|
+
# Called by EM after the connection has started
|
155
|
+
# @private
|
156
|
+
def post_init
|
157
|
+
@connected = true
|
158
|
+
end
|
159
|
+
|
160
|
+
# Called by EM when the connection is closed
|
161
|
+
# @private
|
162
|
+
def unbind
|
163
|
+
raise NoConnection unless @connected
|
164
|
+
|
165
|
+
# @keepalive.cancel
|
166
|
+
@state = :stopped
|
167
|
+
@client.receive_data @error if @error
|
168
|
+
@client.unbind
|
169
|
+
end
|
170
|
+
|
171
|
+
# Called by the parser with parsed nodes
|
172
|
+
# @private
|
173
|
+
def receive(node)
|
174
|
+
Blather.logger.debug "RECEIVING (#{node.element_name}) #{node}"
|
175
|
+
@node = node
|
176
|
+
|
177
|
+
if @node.namespace && @node.namespace.prefix == 'stream'
|
178
|
+
case @node.element_name
|
179
|
+
when 'stream'
|
180
|
+
@state = :ready if @state == :stopped
|
181
|
+
return
|
182
|
+
when 'error'
|
183
|
+
handle_stream_error
|
184
|
+
return
|
185
|
+
when 'end'
|
186
|
+
stop
|
187
|
+
return
|
188
|
+
when 'features'
|
189
|
+
@state = :negotiating
|
190
|
+
@receiver = Features.new(
|
191
|
+
self,
|
192
|
+
proc { ready! },
|
193
|
+
proc { |err| @error = err; stop }
|
194
|
+
)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
@receiver.receive_data @node.to_stanza
|
198
|
+
end
|
199
|
+
|
200
|
+
# Ensure the JID gets attached to the client
|
201
|
+
# @private
|
202
|
+
def jid=(new_jid)
|
203
|
+
Blather.logger.debug "NEW JID: #{new_jid}"
|
204
|
+
@jid = JID.new new_jid
|
205
|
+
end
|
206
|
+
|
207
|
+
protected
|
208
|
+
# Stop the stream
|
209
|
+
# @private
|
210
|
+
def stop
|
211
|
+
unless @state == :stopped
|
212
|
+
@state = :stopped
|
213
|
+
send '</stream:stream>'
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# @private
|
218
|
+
def handle_stream_error
|
219
|
+
@error = StreamError.import(@node)
|
220
|
+
stop
|
221
|
+
end
|
222
|
+
|
223
|
+
# @private
|
224
|
+
def ready!
|
225
|
+
@state = :started
|
226
|
+
@receiver = @client
|
227
|
+
@client.post_init self, @jid
|
228
|
+
end
|
229
|
+
end # Stream
|
230
|
+
|
231
|
+
end # Blather
|