shingara-blather 0.4.8
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.
- 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
|