tp-blather 0.8.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.
- data/.autotest +13 -0
- data/.gemtest +0 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +249 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +413 -0
- data/Rakefile +20 -0
- data/TODO.md +2 -0
- data/blather.gemspec +51 -0
- data/examples/certs/README +20 -0
- data/examples/certs/ca-bundle.crt +3987 -0
- data/examples/echo.rb +19 -0
- data/examples/execute.rb +17 -0
- data/examples/ping_pong.rb +38 -0
- data/examples/print_hierarchy.rb +77 -0
- data/examples/rosterprint.rb +15 -0
- data/examples/stream_only.rb +28 -0
- data/examples/trusted_echo.rb +21 -0
- data/examples/xmpp4r/echo.rb +36 -0
- data/lib/blather.rb +112 -0
- data/lib/blather/cert_store.rb +53 -0
- data/lib/blather/client.rb +95 -0
- data/lib/blather/client/client.rb +345 -0
- data/lib/blather/client/dsl.rb +320 -0
- data/lib/blather/client/dsl/pubsub.rb +174 -0
- data/lib/blather/core_ext/eventmachine.rb +125 -0
- data/lib/blather/core_ext/ipaddr.rb +20 -0
- data/lib/blather/errors.rb +69 -0
- data/lib/blather/errors/sasl_error.rb +44 -0
- data/lib/blather/errors/stanza_error.rb +110 -0
- data/lib/blather/errors/stream_error.rb +84 -0
- data/lib/blather/file_transfer.rb +107 -0
- data/lib/blather/file_transfer/ibb.rb +68 -0
- data/lib/blather/file_transfer/s5b.rb +114 -0
- data/lib/blather/jid.rb +141 -0
- data/lib/blather/roster.rb +118 -0
- data/lib/blather/roster_item.rb +146 -0
- data/lib/blather/stanza.rb +167 -0
- data/lib/blather/stanza/disco.rb +32 -0
- data/lib/blather/stanza/disco/capabilities.rb +161 -0
- data/lib/blather/stanza/disco/disco_info.rb +205 -0
- data/lib/blather/stanza/disco/disco_items.rb +134 -0
- data/lib/blather/stanza/iq.rb +144 -0
- data/lib/blather/stanza/iq/command.rb +339 -0
- data/lib/blather/stanza/iq/ibb.rb +86 -0
- data/lib/blather/stanza/iq/ping.rb +50 -0
- data/lib/blather/stanza/iq/query.rb +53 -0
- data/lib/blather/stanza/iq/roster.rb +185 -0
- data/lib/blather/stanza/iq/s5b.rb +208 -0
- data/lib/blather/stanza/iq/si.rb +415 -0
- data/lib/blather/stanza/iq/vcard.rb +149 -0
- data/lib/blather/stanza/message.rb +428 -0
- data/lib/blather/stanza/message/muc_user.rb +119 -0
- data/lib/blather/stanza/muc/muc_user_base.rb +54 -0
- data/lib/blather/stanza/presence.rb +172 -0
- data/lib/blather/stanza/presence/c.rb +100 -0
- data/lib/blather/stanza/presence/muc.rb +35 -0
- data/lib/blather/stanza/presence/muc_user.rb +147 -0
- data/lib/blather/stanza/presence/status.rb +218 -0
- data/lib/blather/stanza/presence/subscription.rb +100 -0
- data/lib/blather/stanza/pubsub.rb +119 -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 +139 -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 +135 -0
- data/lib/blather/stanza/pubsub/subscriptions.rb +83 -0
- data/lib/blather/stanza/pubsub/unsubscribe.rb +84 -0
- data/lib/blather/stanza/pubsub_owner.rb +51 -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/x.rb +416 -0
- data/lib/blather/stream.rb +266 -0
- data/lib/blather/stream/client.rb +32 -0
- data/lib/blather/stream/component.rb +39 -0
- data/lib/blather/stream/features.rb +70 -0
- data/lib/blather/stream/features/register.rb +38 -0
- data/lib/blather/stream/features/resource.rb +63 -0
- data/lib/blather/stream/features/sasl.rb +190 -0
- data/lib/blather/stream/features/session.rb +45 -0
- data/lib/blather/stream/features/tls.rb +29 -0
- data/lib/blather/stream/parser.rb +102 -0
- data/lib/blather/version.rb +3 -0
- data/lib/blather/xmpp_node.rb +94 -0
- data/spec/blather/client/client_spec.rb +687 -0
- data/spec/blather/client/dsl/pubsub_spec.rb +492 -0
- data/spec/blather/client/dsl_spec.rb +266 -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/file_transfer_spec.rb +135 -0
- data/spec/blather/jid_spec.rb +87 -0
- data/spec/blather/roster_item_spec.rb +134 -0
- data/spec/blather/roster_spec.rb +107 -0
- data/spec/blather/stanza/discos/disco_info_spec.rb +247 -0
- data/spec/blather/stanza/discos/disco_items_spec.rb +154 -0
- data/spec/blather/stanza/iq/command_spec.rb +206 -0
- data/spec/blather/stanza/iq/ibb_spec.rb +124 -0
- data/spec/blather/stanza/iq/ping_spec.rb +45 -0
- data/spec/blather/stanza/iq/query_spec.rb +64 -0
- data/spec/blather/stanza/iq/roster_spec.rb +139 -0
- data/spec/blather/stanza/iq/s5b_spec.rb +57 -0
- data/spec/blather/stanza/iq/si_spec.rb +98 -0
- data/spec/blather/stanza/iq/vcard_spec.rb +93 -0
- data/spec/blather/stanza/iq_spec.rb +61 -0
- data/spec/blather/stanza/message/muc_user_spec.rb +152 -0
- data/spec/blather/stanza/message_spec.rb +282 -0
- data/spec/blather/stanza/presence/c_spec.rb +56 -0
- data/spec/blather/stanza/presence/muc_spec.rb +37 -0
- data/spec/blather/stanza/presence/muc_user_spec.rb +83 -0
- data/spec/blather/stanza/presence/status_spec.rb +144 -0
- data/spec/blather/stanza/presence/subscription_spec.rb +102 -0
- data/spec/blather/stanza/presence_spec.rb +125 -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 +98 -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 +74 -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 +68 -0
- data/spec/blather/stanza/x_spec.rb +231 -0
- data/spec/blather/stanza_spec.rb +134 -0
- data/spec/blather/stream/client_spec.rb +1090 -0
- data/spec/blather/stream/component_spec.rb +108 -0
- data/spec/blather/stream/parser_spec.rb +152 -0
- data/spec/blather/stream/ssl_spec.rb +32 -0
- data/spec/blather/xmpp_node_spec.rb +47 -0
- data/spec/blather_spec.rb +34 -0
- data/spec/fixtures/pubsub.rb +311 -0
- data/spec/spec_helper.rb +17 -0
- data/yard/templates/default/class/html/handlers.erb +18 -0
- data/yard/templates/default/class/setup.rb +10 -0
- data/yard/templates/default/class/text/handlers.erb +1 -0
- metadata +459 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Blather
|
|
2
|
+
class Stream
|
|
3
|
+
|
|
4
|
+
# @private
|
|
5
|
+
class Resource < Features
|
|
6
|
+
BIND_NS = 'urn:ietf:params:xml:ns:xmpp-bind'.freeze
|
|
7
|
+
register BIND_NS
|
|
8
|
+
|
|
9
|
+
def initialize(stream, succeed, fail)
|
|
10
|
+
super
|
|
11
|
+
@jid = stream.jid
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def receive_data(stanza)
|
|
15
|
+
@node = stanza
|
|
16
|
+
case stanza.element_name
|
|
17
|
+
when 'bind' then bind
|
|
18
|
+
when 'iq' then result
|
|
19
|
+
else fail!(UnknownResponse.new(@node))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
##
|
|
25
|
+
# Respond to the bind request
|
|
26
|
+
# If @jid has a resource set already request it from the server
|
|
27
|
+
def bind
|
|
28
|
+
response = Stanza::Iq.new :set
|
|
29
|
+
@id = response.id
|
|
30
|
+
|
|
31
|
+
response << (binder = XMPPNode.new('bind', response.document))
|
|
32
|
+
binder.namespace = BIND_NS
|
|
33
|
+
|
|
34
|
+
if @jid.resource
|
|
35
|
+
binder << (resource = XMPPNode.new('resource', binder.document))
|
|
36
|
+
resource.content = @jid.resource
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@stream.send response
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Process the result from the server
|
|
44
|
+
# Sets the sends the JID (now bound to a resource)
|
|
45
|
+
# back to the stream
|
|
46
|
+
def result
|
|
47
|
+
if @node[:type] == 'error'
|
|
48
|
+
fail! StanzaError.import(@node)
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# ensure this is a response to our original request
|
|
53
|
+
if @id == @node['id']
|
|
54
|
+
@stream.jid = JID.new @node.find_first('bind_ns:bind/bind_ns:jid', :bind_ns => BIND_NS).content
|
|
55
|
+
succeed!
|
|
56
|
+
else
|
|
57
|
+
fail!("BIND result ID mismatch. Expected: #{@id}. Received: #{@node['id']}")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end #Resource
|
|
61
|
+
|
|
62
|
+
end #Stream
|
|
63
|
+
end #Blather
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
module Blather
|
|
2
|
+
class Stream
|
|
3
|
+
|
|
4
|
+
# @private
|
|
5
|
+
class SASL < Features
|
|
6
|
+
class UnknownMechanism < BlatherError
|
|
7
|
+
register :sasl_unknown_mechanism
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
MECHANISMS = %w[
|
|
11
|
+
digest-md5
|
|
12
|
+
plain
|
|
13
|
+
anonymous
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
SASL_NS = 'urn:ietf:params:xml:ns:xmpp-sasl'.freeze
|
|
17
|
+
register SASL_NS
|
|
18
|
+
|
|
19
|
+
def initialize(stream, succeed, fail)
|
|
20
|
+
super
|
|
21
|
+
@jid = @stream.jid
|
|
22
|
+
@pass = @stream.password
|
|
23
|
+
@mechanisms = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def receive_data(stanza)
|
|
27
|
+
@node = stanza
|
|
28
|
+
case stanza.element_name
|
|
29
|
+
when 'mechanisms'
|
|
30
|
+
available_mechanisms = stanza.children.map { |m| m.content.downcase }
|
|
31
|
+
@mechanisms = MECHANISMS.select { |m| available_mechanisms.include? m }
|
|
32
|
+
next!
|
|
33
|
+
when 'failure'
|
|
34
|
+
next!
|
|
35
|
+
when 'success'
|
|
36
|
+
@stream.start
|
|
37
|
+
else
|
|
38
|
+
if self.respond_to?(stanza.element_name)
|
|
39
|
+
self.__send__(stanza.element_name)
|
|
40
|
+
else
|
|
41
|
+
fail! UnknownResponse.new(stanza)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
protected
|
|
47
|
+
def next!
|
|
48
|
+
if @jid.node == ''
|
|
49
|
+
process_anonymous
|
|
50
|
+
else
|
|
51
|
+
@idx = @idx ? @idx+1 : 0
|
|
52
|
+
authenticate_with @mechanisms[@idx]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def process_anonymous
|
|
57
|
+
if @mechanisms.include?('anonymous')
|
|
58
|
+
authenticate_with 'anonymous'
|
|
59
|
+
else
|
|
60
|
+
fail! BlatherError.new('The server does not support ANONYMOUS login. You must provide a node in the JID')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def authenticate_with(method)
|
|
65
|
+
method = case method
|
|
66
|
+
when 'digest-md5' then DigestMD5
|
|
67
|
+
when 'plain' then Plain
|
|
68
|
+
when 'anonymous' then Anonymous
|
|
69
|
+
when nil then fail!(SASLError.import(@node))
|
|
70
|
+
else next!
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if method.is_a?(Module)
|
|
74
|
+
extend method
|
|
75
|
+
authenticate
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# Base64 Encoder
|
|
81
|
+
def b64(str)
|
|
82
|
+
[str].pack('m').gsub(/\s/,'')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Builds a standard auth node
|
|
87
|
+
def auth_node(mechanism, content = nil)
|
|
88
|
+
node = XMPPNode.new 'auth'
|
|
89
|
+
node.content = content if content
|
|
90
|
+
node.namespace = SASL_NS
|
|
91
|
+
node[:mechanism] = mechanism
|
|
92
|
+
node
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
##
|
|
96
|
+
# Digest MD5 authentication
|
|
97
|
+
module DigestMD5 # :nodoc:
|
|
98
|
+
##
|
|
99
|
+
# Lets the server know we're going to try DigestMD5 authentication
|
|
100
|
+
def authenticate
|
|
101
|
+
@stream.send auth_node('DIGEST-MD5')
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
##
|
|
105
|
+
# Receive the challenge command.
|
|
106
|
+
def challenge
|
|
107
|
+
decode_challenge
|
|
108
|
+
respond
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
##
|
|
113
|
+
# Decodes digest strings 'foo=bar,baz="faz"'
|
|
114
|
+
# into {'foo' => 'bar', 'baz' => 'faz'}
|
|
115
|
+
def decode_challenge
|
|
116
|
+
text = @node.content.unpack('m').first
|
|
117
|
+
res = {}
|
|
118
|
+
|
|
119
|
+
text.split(',').each do |statement|
|
|
120
|
+
key, value = statement.split('=')
|
|
121
|
+
res[key] = value.delete('"') unless key.empty?
|
|
122
|
+
end
|
|
123
|
+
Blather.log "CHALLENGE DECODE: #{res.inspect}"
|
|
124
|
+
|
|
125
|
+
@nonce ||= res['nonce']
|
|
126
|
+
@realm ||= res['realm']
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
##
|
|
130
|
+
# Builds the properly encoded challenge response
|
|
131
|
+
def generate_response
|
|
132
|
+
a1 = "#{d("#{@response[:username]}:#{@response[:realm]}:#{@pass}")}:#{@response[:nonce]}:#{@response[:cnonce]}"
|
|
133
|
+
a2 = "AUTHENTICATE:#{@response[:'digest-uri']}"
|
|
134
|
+
h("#{h(a1)}:#{@response[:nonce]}:#{@response[:nc]}:#{@response[:cnonce]}:#{@response[:qop]}:#{h(a2)}")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# Send challenge response
|
|
139
|
+
def respond
|
|
140
|
+
node = XMPPNode.new 'response'
|
|
141
|
+
node.namespace = SASL_NS
|
|
142
|
+
|
|
143
|
+
unless @initial_response_sent
|
|
144
|
+
@initial_response_sent = true
|
|
145
|
+
@response = {
|
|
146
|
+
:nonce => @nonce,
|
|
147
|
+
:charset => 'utf-8',
|
|
148
|
+
:username => @jid.node,
|
|
149
|
+
:realm => @realm || @jid.domain,
|
|
150
|
+
:cnonce => h(Time.new.to_f.to_s),
|
|
151
|
+
:nc => '00000001',
|
|
152
|
+
:qop => 'auth',
|
|
153
|
+
:'digest-uri' => "xmpp/#{@jid.domain}",
|
|
154
|
+
}
|
|
155
|
+
@response[:response] = generate_response
|
|
156
|
+
@response.each { |k,v| @response[k] = "\"#{v}\"" unless [:nc, :qop, :response, :charset].include?(k) }
|
|
157
|
+
|
|
158
|
+
Blather.log "CHALLENGE RESPONSE: #{@response.inspect}"
|
|
159
|
+
Blather.log "CH RESP TXT: #{@response.map { |k,v| "#{k}=#{v}" } * ','}"
|
|
160
|
+
|
|
161
|
+
# order is to simplify testing
|
|
162
|
+
# Ruby 1.9 eliminates the need for this with ordered hashes
|
|
163
|
+
order = [:nonce, :charset, :username, :realm, :cnonce, :nc, :qop, :'digest-uri', :response]
|
|
164
|
+
node.content = b64(order.map { |k| v = @response[k]; "#{k}=#{v}" } * ',')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
@stream.send node
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def d(s); Digest::MD5.digest(s); end
|
|
171
|
+
def h(s); Digest::MD5.hexdigest(s); end
|
|
172
|
+
end #DigestMD5
|
|
173
|
+
|
|
174
|
+
# @private
|
|
175
|
+
module Plain
|
|
176
|
+
def authenticate
|
|
177
|
+
@stream.send auth_node('PLAIN', b64("#{@jid.stripped}\x00#{@jid.node}\x00#{@pass}"))
|
|
178
|
+
end
|
|
179
|
+
end #Plain
|
|
180
|
+
|
|
181
|
+
# @private
|
|
182
|
+
module Anonymous
|
|
183
|
+
def authenticate
|
|
184
|
+
@stream.send auth_node('ANONYMOUS')
|
|
185
|
+
end
|
|
186
|
+
end #Anonymous
|
|
187
|
+
end #SASL
|
|
188
|
+
|
|
189
|
+
end #Stream
|
|
190
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Blather
|
|
2
|
+
class Stream
|
|
3
|
+
|
|
4
|
+
# @private
|
|
5
|
+
class Session < Features
|
|
6
|
+
SESSION_NS = 'urn:ietf:params:xml:ns:xmpp-session'.freeze
|
|
7
|
+
register SESSION_NS
|
|
8
|
+
|
|
9
|
+
def initialize(stream, succeed, fail)
|
|
10
|
+
super
|
|
11
|
+
@to = @stream.jid.domain
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def receive_data(stanza)
|
|
15
|
+
@node = stanza
|
|
16
|
+
case stanza.element_name
|
|
17
|
+
when 'session' then session
|
|
18
|
+
when 'iq' then check_response
|
|
19
|
+
else fail!(UnknownResponse.new(stanza))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
def check_response
|
|
25
|
+
if @node[:type] == 'result'
|
|
26
|
+
succeed!
|
|
27
|
+
else
|
|
28
|
+
fail!(StanzaError.import(@node))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Send a start session command
|
|
34
|
+
def session
|
|
35
|
+
response = Stanza::Iq.new :set
|
|
36
|
+
response.to = @to
|
|
37
|
+
response << (sess = XMPPNode.new('session', response.document))
|
|
38
|
+
sess.namespace = SESSION_NS
|
|
39
|
+
|
|
40
|
+
@stream.send response
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Blather
|
|
2
|
+
class Stream
|
|
3
|
+
|
|
4
|
+
# @private
|
|
5
|
+
class TLS < Features
|
|
6
|
+
class TLSFailure < BlatherError
|
|
7
|
+
register :tls_failure
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
TLS_NS = 'urn:ietf:params:xml:ns:xmpp-tls'.freeze
|
|
11
|
+
register TLS_NS
|
|
12
|
+
|
|
13
|
+
def receive_data(stanza)
|
|
14
|
+
case stanza.element_name
|
|
15
|
+
when 'starttls'
|
|
16
|
+
@stream.send "<starttls xmlns='#{TLS_NS}'/>"
|
|
17
|
+
when 'proceed'
|
|
18
|
+
@stream.start_tls(:verify_peer => true)
|
|
19
|
+
@stream.start
|
|
20
|
+
# succeed!
|
|
21
|
+
else
|
|
22
|
+
fail! TLSFailure.new
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end #TLS
|
|
27
|
+
|
|
28
|
+
end #Stream
|
|
29
|
+
end #Blather
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
module Blather
|
|
2
|
+
class Stream
|
|
3
|
+
|
|
4
|
+
# @private
|
|
5
|
+
class Parser < Nokogiri::XML::SAX::Document
|
|
6
|
+
NS_TO_IGNORE = %w[jabber:client jabber:component:accept]
|
|
7
|
+
|
|
8
|
+
@@debug = false
|
|
9
|
+
def self.debug; @@debug; end
|
|
10
|
+
def self.debug=(debug); @@debug = debug; end
|
|
11
|
+
|
|
12
|
+
def initialize(receiver)
|
|
13
|
+
@receiver = receiver
|
|
14
|
+
@current = nil
|
|
15
|
+
@namespaces = {}
|
|
16
|
+
@namespace_definitions = []
|
|
17
|
+
@parser = Nokogiri::XML::SAX::PushParser.new self
|
|
18
|
+
@parser.options = Nokogiri::XML::ParseOptions::DEFAULT_XML | Nokogiri::XML::ParseOptions::NOENT
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def receive_data(string)
|
|
22
|
+
Blather.log "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.log "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
|
+
ns_keys = namespaces.map { |pre, href| pre }
|
|
41
|
+
namespaces.delete_if { |pre, href| NS_TO_IGNORE.include? href }
|
|
42
|
+
@namespace_definitions.push []
|
|
43
|
+
namespaces.each do |pre, href|
|
|
44
|
+
next if @namespace_definitions.flatten.include?(@namespaces[[pre, href]])
|
|
45
|
+
ns = node.add_namespace(pre, href)
|
|
46
|
+
@namespaces[[pre, href]] ||= ns
|
|
47
|
+
end
|
|
48
|
+
@namespaces[[prefix, uri]] ||= node.add_namespace(prefix, uri) if prefix && !ns_keys.include?(prefix)
|
|
49
|
+
node.namespace = @namespaces[[prefix, uri]]
|
|
50
|
+
|
|
51
|
+
unless @receiver.stopped?
|
|
52
|
+
@current << node if @current
|
|
53
|
+
@current = node
|
|
54
|
+
end
|
|
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.log "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.log "CHARS: #{chars}" if @@debug
|
|
83
|
+
@current << Nokogiri::XML::Text.new(chars, @current.document) if @current
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def warning(msg)
|
|
87
|
+
Blather.log "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,94 @@
|
|
|
1
|
+
module Blather
|
|
2
|
+
|
|
3
|
+
# Base XML Node
|
|
4
|
+
# All XML classes subclass XMPPNode it allows the addition of helpers
|
|
5
|
+
class XMPPNode < Niceogiri::XML::Node
|
|
6
|
+
# @private
|
|
7
|
+
BASE_NAMES = %w[presence message iq].freeze
|
|
8
|
+
|
|
9
|
+
# @private
|
|
10
|
+
@@registrations = {}
|
|
11
|
+
|
|
12
|
+
class_attribute :registered_ns, :registered_name
|
|
13
|
+
|
|
14
|
+
# Register a new stanza class to a name and/or namespace
|
|
15
|
+
#
|
|
16
|
+
# This registers a namespace that is used when looking
|
|
17
|
+
# up the class name of the object to instantiate when a new
|
|
18
|
+
# stanza is received
|
|
19
|
+
#
|
|
20
|
+
# @param [#to_s] name the name of the node
|
|
21
|
+
# @param [String, nil] ns the namespace the node belongs to
|
|
22
|
+
def self.register(name, ns = nil)
|
|
23
|
+
self.registered_name = name.to_s
|
|
24
|
+
self.registered_ns = ns
|
|
25
|
+
@@registrations[[self.registered_name, self.registered_ns]] = self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Find the class to use given the name and namespace of a stanza
|
|
29
|
+
#
|
|
30
|
+
# @param [#to_s] name the name to lookup
|
|
31
|
+
# @param [String, nil] xmlns the namespace the node belongs to
|
|
32
|
+
# @return [Class, nil] the class appropriate for the name/ns combination
|
|
33
|
+
def self.class_from_registration(name, ns = nil)
|
|
34
|
+
@@registrations[[name.to_s, ns]]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Import an XML::Node to the appropriate class
|
|
38
|
+
#
|
|
39
|
+
# Looks up the class the node should be then creates it based on the
|
|
40
|
+
# elements of the XML::Node
|
|
41
|
+
# @param [XML::Node] node the node to import
|
|
42
|
+
# @return the appropriate object based on the node name and namespace
|
|
43
|
+
def self.import(node, *decorators)
|
|
44
|
+
ns = (node.namespace.href if node.namespace)
|
|
45
|
+
klass = class_from_registration(node.element_name, ns)
|
|
46
|
+
if klass && klass != self
|
|
47
|
+
klass.import(node, *decorators)
|
|
48
|
+
else
|
|
49
|
+
new(node.element_name).decorate(*decorators).inherit(node)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Parse a string as XML and import to the appropriate class
|
|
54
|
+
#
|
|
55
|
+
# @param [String] string the string to parse
|
|
56
|
+
# @return the appropriate object based on the node name and namespace
|
|
57
|
+
def self.parse(string)
|
|
58
|
+
import Nokogiri::XML(string).root
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Create a new Node object
|
|
62
|
+
#
|
|
63
|
+
# @param [String, nil] name the element name
|
|
64
|
+
# @param [XML::Document, nil] doc the document to attach the node to. If
|
|
65
|
+
# not provided one will be created
|
|
66
|
+
# @return a new object with the registered name and namespace
|
|
67
|
+
def self.new(name = registered_name, doc = nil)
|
|
68
|
+
super name, doc, BASE_NAMES.include?(name.to_s) ? nil : self.registered_ns
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.decorator_modules
|
|
72
|
+
[self::InstanceMethods]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def decorate(*decorators)
|
|
76
|
+
decorators.each do |decorator|
|
|
77
|
+
decorator.decorator_modules.each do |mod|
|
|
78
|
+
extend mod
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@handler_hierarchy.unshift decorator.handler_hierarchy.first
|
|
82
|
+
end
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Turn the object into a proper stanza
|
|
87
|
+
#
|
|
88
|
+
# @return a stanza object
|
|
89
|
+
def to_stanza
|
|
90
|
+
self.class.import self
|
|
91
|
+
end
|
|
92
|
+
end # XMPPNode
|
|
93
|
+
|
|
94
|
+
end # Blather
|