blather 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/CHANGELOG +1 -0
  2. data/LICENSE +20 -0
  3. data/Manifest +43 -0
  4. data/README.rdoc +78 -0
  5. data/Rakefile +16 -0
  6. data/blather.gemspec +41 -0
  7. data/examples/echo.rb +22 -0
  8. data/examples/shell_client.rb +28 -0
  9. data/lib/autotest/discover.rb +1 -0
  10. data/lib/autotest/spec.rb +60 -0
  11. data/lib/blather.rb +46 -0
  12. data/lib/blather/callback.rb +24 -0
  13. data/lib/blather/client.rb +81 -0
  14. data/lib/blather/core/errors.rb +24 -0
  15. data/lib/blather/core/jid.rb +101 -0
  16. data/lib/blather/core/roster.rb +84 -0
  17. data/lib/blather/core/roster_item.rb +92 -0
  18. data/lib/blather/core/stanza.rb +116 -0
  19. data/lib/blather/core/stanza/iq.rb +27 -0
  20. data/lib/blather/core/stanza/iq/query.rb +42 -0
  21. data/lib/blather/core/stanza/iq/roster.rb +96 -0
  22. data/lib/blather/core/stanza/message.rb +55 -0
  23. data/lib/blather/core/stanza/presence.rb +35 -0
  24. data/lib/blather/core/stanza/presence/status.rb +77 -0
  25. data/lib/blather/core/stanza/presence/subscription.rb +73 -0
  26. data/lib/blather/core/stream.rb +181 -0
  27. data/lib/blather/core/stream/parser.rb +74 -0
  28. data/lib/blather/core/stream/resource.rb +51 -0
  29. data/lib/blather/core/stream/sasl.rb +135 -0
  30. data/lib/blather/core/stream/session.rb +43 -0
  31. data/lib/blather/core/stream/tls.rb +29 -0
  32. data/lib/blather/core/sugar.rb +150 -0
  33. data/lib/blather/core/xmpp_node.rb +132 -0
  34. data/lib/blather/extensions.rb +4 -0
  35. data/lib/blather/extensions/last_activity.rb +55 -0
  36. data/lib/blather/extensions/version.rb +85 -0
  37. data/spec/blather/core/jid_spec.rb +78 -0
  38. data/spec/blather/core/roster_item_spec.rb +80 -0
  39. data/spec/blather/core/roster_spec.rb +79 -0
  40. data/spec/blather/core/stanza_spec.rb +95 -0
  41. data/spec/blather/core/stream_spec.rb +263 -0
  42. data/spec/blather/core/xmpp_node_spec.rb +130 -0
  43. data/spec/build_safe.rb +20 -0
  44. data/spec/spec_helper.rb +49 -0
  45. metadata +172 -0
@@ -0,0 +1,95 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
2
+
3
+ describe 'Blather::Stanza' do
4
+ it 'provides .next_id helper for generating new IDs' do
5
+ proc { Stanza.next_id }.must_change 'Stanza', :next_id
6
+ end
7
+
8
+ it 'can import a node' do
9
+ s = Stanza.import XMPPNode.new('foo')
10
+ s.element_name.must_equal 'foo'
11
+ end
12
+
13
+ it 'sets the ID when created' do
14
+ Stanza.new('message').id.wont_be_nil
15
+ end
16
+
17
+ it 'sets the document when created' do
18
+ Stanza.new('message').doc.wont_be_nil
19
+ end
20
+
21
+ it 'provides an #error? helper' do
22
+ s = Stanza.new('message')
23
+ s.error?.must_equal false
24
+ s.type = :error
25
+ s.error?.must_equal true
26
+ end
27
+
28
+ it 'will generate a reply' do
29
+ s = Stanza.new('message')
30
+ s.from = f = JID.new('n@d/r')
31
+ s.to = t = JID.new('d@n/r')
32
+
33
+ r = s.reply
34
+ r.object_id.wont_equal s.object_id
35
+ r.from.must_equal t
36
+ r.to.must_equal f
37
+ end
38
+
39
+ it 'convert to a reply' do
40
+ s = Stanza.new('message')
41
+ s.from = f = JID.new('n@d/r')
42
+ s.to = t = JID.new('d@n/r')
43
+
44
+ r = s.reply!
45
+ r.object_id.must_equal s.object_id
46
+ r.from.must_equal t
47
+ r.to.must_equal f
48
+ end
49
+
50
+ it 'provides "attr_accessor" for id' do
51
+ s = Stanza.new('message')
52
+ s.id.wont_be_nil
53
+ s['id'].wont_be_nil
54
+
55
+ s.id = nil
56
+ s.id.must_be_nil
57
+ s['id'].must_be_nil
58
+ end
59
+
60
+ it 'provides "attr_accessor" for to' do
61
+ s = Stanza.new('message')
62
+ s.to.must_be_nil
63
+ s['to'].must_be_nil
64
+
65
+ s.to = JID.new('n@d/r')
66
+ s.to.wont_be_nil
67
+ s.to.must_be_kind_of JID
68
+
69
+ s['to'].wont_be_nil
70
+ s['to'].must_equal 'n@d/r'
71
+ end
72
+
73
+ it 'provides "attr_accessor" for from' do
74
+ s = Stanza.new('message')
75
+ s.from.must_be_nil
76
+ s['from'].must_be_nil
77
+
78
+ s.from = JID.new('n@d/r')
79
+ s.from.wont_be_nil
80
+ s.from.must_be_kind_of JID
81
+
82
+ s['from'].wont_be_nil
83
+ s['from'].must_equal 'n@d/r'
84
+ end
85
+
86
+ it 'provides "attr_accessor" for type' do
87
+ s = Stanza.new('message')
88
+ s.type.must_be_nil
89
+ s['type'].must_be_nil
90
+
91
+ s.type = 'testing'
92
+ s.type.wont_be_nil
93
+ s['type'].wont_be_nil
94
+ end
95
+ end
@@ -0,0 +1,263 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
2
+
3
+ describe 'Blather::Stream' do
4
+ class MockStream; include Stream; end
5
+ def mock_stream(&block)
6
+ @client = mock()
7
+ @client.stubs(:jid=)
8
+ stream = MockStream.new @client, JID.new('n@d/r'), 'pass'
9
+
10
+ stream.expects(:send_data).at_least(1).with &block
11
+ stream
12
+ end
13
+
14
+ it 'can be started' do
15
+ client = mock()
16
+ params = [client, 'n@d/r', 'pass', 'host', 1234]
17
+ EM.expects(:connect).with do |*parms|
18
+ parms[0] == 'host' &&
19
+ parms[1] == 1234 &&
20
+ parms[3] == client &&
21
+ parms[5] == 'pass' &&
22
+ parms[4] == JID.new('n@d/r')
23
+ end
24
+
25
+ Stream.start *(params)
26
+ end
27
+
28
+ it 'can figure out the host to use based on the jid' do
29
+ client = mock()
30
+ params = [client, 'n@d/r', 'pass', 'd', 5222]
31
+ EM.expects(:connect).with do |*parms|
32
+ parms[0] == 'd' &&
33
+ parms[1] == 5222 &&
34
+ parms[3] == client &&
35
+ parms[5] == 'pass' &&
36
+ parms[4] == JID.new('n@d/r')
37
+ end
38
+
39
+ Stream.start client, 'n@d/r', 'pass'
40
+ end
41
+
42
+ it 'starts the stream once the connection is complete' do
43
+ s = mock_stream { |d| d =~ /stream:stream/ }
44
+ s.connection_completed
45
+ end
46
+
47
+ it 'starts TLS when asked' do
48
+ state = nil
49
+ @stream = mock_stream do |val|
50
+ case
51
+ when state.nil? && val =~ /stream:stream/ then state = :started
52
+ when state == :started && val =~ /starttls/ then true
53
+ else false
54
+ end
55
+ end
56
+ @stream.connection_completed
57
+ @stream.receive_data "<stream:stream><stream:features><starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' /></stream:features>"
58
+ end
59
+
60
+ it 'connects via SASL MD5 when asked' do
61
+ Time.any_instance.stubs(:to_f).returns(1.1)
62
+
63
+ state = nil
64
+ client = mock()
65
+ client.stubs(:jid=)
66
+ stream = MockStream.new client, JID.new('n@d/r'), 'pass'
67
+
68
+ stream.expects(:send_data).times(5).with do |val|
69
+ case state
70
+ when nil
71
+ val.must_match(/stream:stream/)
72
+ state = :started
73
+ stream.receive_data "<stream:stream><stream:features><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>DIGEST-MD5</mechanism></mechanisms></stream:features>"
74
+ true
75
+
76
+ when :started
77
+ val.must_match(/auth.*DIGEST\-MD5/)
78
+ state = :auth_sent
79
+ stream.receive_data "<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cmVhbG09InNvbWVyZWFsbSIsbm9uY2U9Ik9BNk1HOXRFUUdtMmhoIixxb3A9ImF1dGgiLGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNzCg==</challenge>"
80
+ true
81
+
82
+ when :auth_sent
83
+ val.must_equal('<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">bm9uY2U9Ik9BNk1HOXRFUUdtMmhoIixjaGFyc2V0PXV0Zi04LHVzZXJuYW1lPSJuIixyZWFsbT0ic29tZXJlYWxtIixjbm9uY2U9Ijc3N2Q0NWJiYmNkZjUwZDQ5YzQyYzcwYWQ3YWNmNWZlIixuYz0wMDAwMDAwMSxxb3A9YXV0aCxkaWdlc3QtdXJpPSJ4bXBwL2Qi</response>')
84
+ state = :response1_sent
85
+ stream.receive_data "<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZAo=</challenge>"
86
+ true
87
+
88
+ when :response1_sent
89
+ val.must_equal('<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>')
90
+ state = :response2_sent
91
+ stream.receive_data "<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>"
92
+ true
93
+
94
+ when :response2_sent
95
+ val.must_match(/stream:stream/)
96
+ state = :complete
97
+ true
98
+
99
+ else
100
+ false
101
+
102
+ end
103
+ end
104
+ stream.connection_completed
105
+ end
106
+
107
+ it 'will connect via SSL PLAIN when asked' do
108
+ state = nil
109
+ client = mock()
110
+ client.stubs(:jid=)
111
+ stream = MockStream.new client, JID.new('n@d/r'), 'pass'
112
+
113
+ stream.expects(:send_data).times(3).with do |val|
114
+ case state
115
+ when nil
116
+ val.must_match(/stream:stream/)
117
+ state = :started
118
+ stream.receive_data "<stream:stream><stream:features><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>PLAIN</mechanism></mechanisms></stream:features>"
119
+ true
120
+
121
+ when :started
122
+ val.must_equal('<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">bkBkAG4AcGFzcw==</auth>')
123
+ state = :auth_sent
124
+ stream.receive_data "<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>"
125
+ true
126
+
127
+ when :auth_sent
128
+ val.must_match(/stream:stream/)
129
+ state = :complete
130
+ true
131
+
132
+ else
133
+ false
134
+
135
+ end
136
+ end
137
+ stream.connection_completed
138
+ end
139
+
140
+ it 'will connect via SSL ANONYMOUS when asked' do
141
+ state = nil
142
+ client = mock()
143
+ client.stubs(:jid=)
144
+ stream = MockStream.new client, JID.new('n@d/r'), 'pass'
145
+
146
+ stream.expects(:send_data).times(3).with do |val|
147
+ case state
148
+ when nil
149
+ val.must_match(/stream:stream/)
150
+ state = :started
151
+ stream.receive_data "<stream:stream><stream:features><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>ANONYMOUS</mechanism></mechanisms></stream:features>"
152
+ true
153
+
154
+ when :started
155
+ val.must_equal('<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="ANONYMOUS">bg==</auth>')
156
+ state = :auth_sent
157
+ stream.receive_data "<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>"
158
+ true
159
+
160
+ when :auth_sent
161
+ val.must_match(/stream:stream/)
162
+ state = :complete
163
+ true
164
+
165
+ else
166
+ false
167
+
168
+ end
169
+ end
170
+ stream.connection_completed
171
+ end
172
+
173
+ it 'will bind to a resource set by the server' do
174
+ state = nil
175
+ class Client; attr_accessor :jid; end
176
+ client = Client.new
177
+
178
+ jid = JID.new('n@d')
179
+ stream = MockStream.new client, jid, 'pass'
180
+
181
+ stream.expects(:send_data).times(2).with do |val|
182
+ case state
183
+ when nil
184
+ val.must_match(/stream:stream/)
185
+ state = :started
186
+ stream.receive_data "<stream:stream><stream:features><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></stream:features>"
187
+ true
188
+
189
+ when :started
190
+ val.must_match(%r{<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"\s*/>})
191
+ val =~ %r{<iq[^>]+id="([^"]+)"}
192
+ state = :complete
193
+ stream.receive_data "<iq type='result' id='#{$1}'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><jid>#{jid}/server_resource</jid></bind></iq>"
194
+ client.jid.must_equal JID.new('n@d/server_resource')
195
+ true
196
+
197
+ else
198
+ false
199
+
200
+ end
201
+ end
202
+ stream.connection_completed
203
+ end
204
+
205
+ it 'will bind to a resource set by the client' do
206
+ state = nil
207
+ class Client; attr_accessor :jid; end
208
+ client = Client.new
209
+
210
+ jid = JID.new('n@d/r')
211
+ stream = MockStream.new client, jid, 'pass'
212
+
213
+ stream.expects(:send_data).times(2).with do |val|
214
+ case state
215
+ when nil
216
+ val.must_match(/stream:stream/)
217
+ state = :started
218
+ stream.receive_data "<stream:stream><stream:features><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></stream:features>"
219
+ true
220
+
221
+ when :started
222
+ val.must_match(%r{<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>r</resource></bind>})
223
+ val =~ %r{<iq[^>]+id="([^"]+)"}
224
+ state = :complete
225
+ stream.receive_data "<iq type='result' id='#{$1}'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><jid>#{jid}</jid></bind></iq>"
226
+ client.jid.must_equal JID.new('n@d/r')
227
+ true
228
+
229
+ else
230
+ false
231
+
232
+ end
233
+ end
234
+ stream.connection_completed
235
+ end
236
+
237
+ it 'will establish a session if requested' do
238
+ state = nil
239
+ client = mock()
240
+ client.stubs(:jid=)
241
+ stream = MockStream.new client, JID.new('n@d/r'), 'pass'
242
+
243
+ stream.expects(:send_data).times(2).with do |val|
244
+ case state
245
+ when nil
246
+ val.must_match(/stream:stream/)
247
+ state = :started
248
+ stream.receive_data "<stream:stream><stream:features><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></stream:features>"
249
+ true
250
+
251
+ when :started
252
+ val.must_match('<iq id="[^"]+" type="set" to="d"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>')
253
+ state = :completed
254
+ true
255
+
256
+ else
257
+ false
258
+
259
+ end
260
+ end
261
+ stream.connection_completed
262
+ end
263
+ end
@@ -0,0 +1,130 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
2
+
3
+ describe 'Blather::XMPPNode' do
4
+ it 'generates a new node' do
5
+ n = XMPPNode.new 'foo'
6
+ n.element_name.must_equal 'foo'
7
+ end
8
+
9
+ it 'generates a node based on the current name' do
10
+ class Foo < XMPPNode; end
11
+ Foo.name = 'foo'
12
+ Foo.new.element_name.must_equal 'foo'
13
+ end
14
+
15
+ it 'sets the namespace on creation' do
16
+ class Foo < XMPPNode; end
17
+ Foo.xmlns = 'foo'
18
+ Foo.new('foo').xmlns.must_equal 'foo'
19
+ end
20
+
21
+ it 'registers sub classes' do
22
+ class Foo < XMPPNode; register 'foo', 'foo:bar'; end
23
+ Foo.name.must_equal 'foo'
24
+ Foo.xmlns.must_equal 'foo:bar'
25
+ XMPPNode.class_from_registration('foo', 'foo:bar').must_equal Foo
26
+ end
27
+
28
+ it 'imports another node' do
29
+ class Foo < XMPPNode; register 'foo', 'foo:bar'; end
30
+ n = XMPPNode.new('foo')
31
+ n.xmlns = 'foo:bar'
32
+ XMPPNode.import(n).must_be_kind_of Foo
33
+ end
34
+
35
+ it 'can convert itself into a stanza' do
36
+ class Foo < XMPPNode; register 'foo'; end
37
+ n = XMPPNode.new('foo')
38
+ n.to_stanza.must_be_kind_of Foo
39
+ end
40
+
41
+ it 'provides "attr_accessor" for xmlns' do
42
+ n = XMPPNode.new('foo')
43
+ n.xmlns.must_be_nil
44
+ n['xmlns'].must_be_nil
45
+
46
+ n.xmlns = 'foo:bar'
47
+ n.xmlns.must_equal 'foo:bar'
48
+ n['xmlns'].must_equal 'foo:bar'
49
+ end
50
+
51
+ it 'will remove a child element' do
52
+ n = XMPPNode.new 'foo'
53
+ n << XMPPNode.new('bar')
54
+ n << XMPPNode.new('bar')
55
+
56
+ n.find('bar').size.must_equal 2
57
+ n.remove_child 'bar'
58
+ n.find('bar').size.must_equal 1
59
+ end
60
+
61
+ it 'will remove a child with a specific xmlns' do
62
+ n = XMPPNode.new 'foo'
63
+ n << XMPPNode.new('bar')
64
+ c = XMPPNode.new('bar')
65
+ c.xmlns = 'foo:bar'
66
+ n << c
67
+
68
+ n.find('bar').size.must_equal 2
69
+ n.remove_child 'bar', 'foo:bar'
70
+ n.find('bar').size.must_equal 1
71
+ n.find('bar').first.xmlns.must_be_nil
72
+ end
73
+
74
+ it 'will remove all child elements' do
75
+ n = XMPPNode.new 'foo'
76
+ n << XMPPNode.new('bar')
77
+ n << XMPPNode.new('bar')
78
+
79
+ n.find('bar').size.must_equal 2
80
+ n.remove_children 'bar'
81
+ n.find('bar').size.must_equal 0
82
+ end
83
+
84
+ it 'provides a helper to grab content from a child' do
85
+ n = XMPPNode.new 'foo'
86
+ n << XMPPNode.new('bar', 'baz')
87
+ n.content_from(:bar).must_equal 'baz'
88
+ end
89
+
90
+ it 'provides a copy mechanism' do
91
+ n = XMPPNode.new 'foo'
92
+ n2 = n.copy
93
+ n2.object_id.wont_equal n.object_id
94
+ n2.element_name.must_equal n.element_name
95
+ end
96
+
97
+ it 'provides an inhert mechanism' do
98
+ n = XMPPNode.new 'foo'
99
+ n2 = XMPPNode.new 'foo', 'bar'
100
+ n2['foo'] = 'bar'
101
+
102
+ n.inherit(n2)
103
+ n['foo'].must_equal 'bar'
104
+ n.content.must_equal 'bar'
105
+ end
106
+
107
+ it 'provides a mechanism to inherit attrs' do
108
+ n = XMPPNode.new 'foo'
109
+ n2 = XMPPNode.new 'foo'
110
+ n2['foo'] = 'bar'
111
+
112
+ n.inherit_attrs(n2.attributes)
113
+ n['foo'].must_equal 'bar'
114
+ end
115
+
116
+ it 'cuts line breaks out of #to_s' do
117
+ n = XMPPNode.new 'foo'
118
+ n << XMPPNode.new('bar', 'baz')
119
+ n.to_s.scan(">\n<").size.must_equal 0
120
+ end
121
+
122
+ it 'overrides #find to find without xpath' do
123
+ n = XMPPNode.new 'foo'
124
+ n << XMPPNode.new('bar', 'baz')
125
+ n.find('bar').must_be_kind_of Array
126
+
127
+ XML::Document.new.root = n
128
+ n.find('bar').must_be_kind_of XML::XPath::Object
129
+ end
130
+ end