blather 0.2.1 → 0.2.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.
Files changed (84) hide show
  1. data/LICENSE +2 -0
  2. data/README.rdoc +54 -29
  3. data/Rakefile +94 -13
  4. data/VERSION.yml +4 -0
  5. data/examples/drb_client.rb +2 -4
  6. data/examples/echo.rb +13 -8
  7. data/examples/pubsub/cli.rb +64 -0
  8. data/examples/pubsub/ping_pong.rb +18 -0
  9. data/examples/pubsub/pubsub_dsl.rb +52 -0
  10. data/examples/pubsub_client.rb +39 -0
  11. data/examples/rosterprint.rb +14 -0
  12. data/examples/xmpp4r/echo.rb +35 -0
  13. data/ext/extconf.rb +65 -0
  14. data/lib/blather.rb +18 -121
  15. data/lib/blather/client.rb +13 -0
  16. data/lib/blather/client/client.rb +165 -0
  17. data/lib/blather/client/dsl.rb +99 -0
  18. data/lib/blather/client/pubsub.rb +53 -0
  19. data/lib/blather/client/pubsub/node.rb +27 -0
  20. data/lib/blather/core_ext/active_support.rb +1 -0
  21. data/lib/blather/core_ext/libxml.rb +7 -1
  22. data/lib/blather/errors.rb +39 -18
  23. data/lib/blather/errors/sasl_error.rb +87 -0
  24. data/lib/blather/errors/stanza_error.rb +262 -0
  25. data/lib/blather/errors/stream_error.rb +253 -0
  26. data/lib/blather/jid.rb +9 -16
  27. data/lib/blather/roster.rb +9 -0
  28. data/lib/blather/roster_item.rb +7 -4
  29. data/lib/blather/stanza.rb +19 -25
  30. data/lib/blather/stanza/disco.rb +9 -0
  31. data/lib/blather/stanza/disco/disco_info.rb +84 -0
  32. data/lib/blather/stanza/disco/disco_items.rb +59 -0
  33. data/lib/blather/stanza/iq.rb +16 -4
  34. data/lib/blather/stanza/iq/query.rb +6 -4
  35. data/lib/blather/stanza/iq/roster.rb +38 -38
  36. data/lib/blather/stanza/pubsub.rb +33 -0
  37. data/lib/blather/stanza/pubsub/affiliations.rb +52 -0
  38. data/lib/blather/stanza/pubsub/errors.rb +9 -0
  39. data/lib/blather/stanza/pubsub/event.rb +21 -0
  40. data/lib/blather/stanza/pubsub/items.rb +59 -0
  41. data/lib/blather/stanza/pubsub/owner.rb +9 -0
  42. data/lib/blather/stanza/pubsub/subscriptions.rb +57 -0
  43. data/lib/blather/stream.rb +125 -57
  44. data/lib/blather/stream/client.rb +26 -0
  45. data/lib/blather/stream/component.rb +34 -0
  46. data/lib/blather/stream/parser.rb +17 -27
  47. data/lib/blather/stream/resource.rb +21 -24
  48. data/lib/blather/stream/sasl.rb +60 -37
  49. data/lib/blather/stream/session.rb +12 -19
  50. data/lib/blather/stream/stream_handler.rb +39 -0
  51. data/lib/blather/stream/tls.rb +22 -18
  52. data/lib/blather/xmpp_node.rb +91 -17
  53. data/spec/blather/core_ext/libxml_spec.rb +58 -0
  54. data/spec/blather/errors/sasl_error_spec.rb +56 -0
  55. data/spec/blather/errors/stanza_error_spec.rb +148 -0
  56. data/spec/blather/errors/stream_error_spec.rb +114 -0
  57. data/spec/blather/errors_spec.rb +40 -0
  58. data/spec/blather/jid_spec.rb +0 -7
  59. data/spec/blather/roster_item_spec.rb +5 -0
  60. data/spec/blather/roster_spec.rb +6 -6
  61. data/spec/blather/stanza/discos/disco_info_spec.rb +207 -0
  62. data/spec/blather/stanza/discos/disco_items_spec.rb +136 -0
  63. data/spec/blather/stanza/iq/query_spec.rb +9 -2
  64. data/spec/blather/stanza/iq/roster_spec.rb +117 -1
  65. data/spec/blather/stanza/iq_spec.rb +29 -0
  66. data/spec/blather/stanza/presence/subscription_spec.rb +12 -1
  67. data/spec/blather/stanza/presence_spec.rb +29 -0
  68. data/spec/blather/stanza/pubsub/affiliations_spec.rb +46 -0
  69. data/spec/blather/stanza/pubsub/items_spec.rb +59 -0
  70. data/spec/blather/stanza/pubsub/subscriptions_spec.rb +63 -0
  71. data/spec/blather/stanza/pubsub_spec.rb +26 -0
  72. data/spec/blather/stanza_spec.rb +13 -1
  73. data/spec/blather/stream/client_spec.rb +787 -0
  74. data/spec/blather/stream/component_spec.rb +86 -0
  75. data/spec/blather/xmpp_node_spec.rb +75 -22
  76. data/spec/fixtures/pubsub.rb +157 -0
  77. data/spec/spec_helper.rb +6 -14
  78. metadata +86 -74
  79. data/CHANGELOG +0 -5
  80. data/Manifest +0 -47
  81. data/blather.gemspec +0 -41
  82. data/lib/blather/stanza/error.rb +0 -31
  83. data/spec/blather/stream_spec.rb +0 -462
  84. data/spec/build_safe.rb +0 -20
@@ -1,66 +1,68 @@
1
1
  module Blather # :nodoc:
2
- module Stream # :nodoc:
2
+ class Stream # :nodoc:
3
3
 
4
- class SASL # :nodoc:
5
- class UnknownMechanism < BlatherError; end
4
+ class SASL < StreamHandler # :nodoc:
5
+ class UnknownMechanism < BlatherError
6
+ handler_heirarchy ||= []
7
+ handler_heirarchy << :unknown_mechanism
8
+ end
6
9
 
7
- SASL_NS = 'urn:ietf:params:xml:ns:xmpp-sasl'
10
+ SASL_NS = 'urn:ietf:params:xml:ns:xmpp-sasl'.freeze
8
11
 
9
12
  def initialize(stream, jid, pass = nil)
10
- @stream = stream
13
+ super stream
11
14
  @jid = jid
12
15
  @pass = pass
13
- @callbacks = {}
14
- @mechanism = 0
16
+ @mechanism_idx = 0
15
17
  @mechanisms = []
16
-
17
- init_callbacks
18
- end
19
-
20
- def init_callbacks
21
- @callbacks['mechanisms'] = proc {
22
- @mechanisms = @node.children
23
- set_mechanism
24
- authenticate
25
- }
26
18
  end
27
19
 
28
20
  def set_mechanism
29
- mod = case (mechanism = @mechanisms[@mechanism].content)
21
+ mod = case (mechanism = @mechanisms[@mechanism_idx].content)
30
22
  when 'DIGEST-MD5' then DigestMD5
31
23
  when 'PLAIN' then Plain
32
24
  when 'ANONYMOUS' then Anonymous
33
25
  else
26
+ # Send a failure node and kill the stream
34
27
  @stream.send "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><invalid-mechanism/></failure>"
35
- raise UnknownMechanism, "Unknown SASL mechanism (#{mechanism})"
28
+ @failure.call UnknownMechanism.new("Unknown SASL mechanism (#{mechanism})")
29
+ return false
36
30
  end
37
31
 
38
32
  extend mod
33
+ true
39
34
  end
40
35
 
41
- def receive(node)
42
- @node = node
43
- if @node.element_name == 'failure' && @mechanisms[@mechanism += 1]
44
- set_mechanism
45
- authenticate
36
+ ##
37
+ # Handle incoming nodes
38
+ # Cycle through possible mechanisms until we either
39
+ # run out of them or none work
40
+ def handle(node)
41
+ if node.element_name == 'failure'
42
+ if @mechanisms[@mechanism_idx += 1]
43
+ set_mechanism
44
+ authenticate
45
+ else
46
+ failure node
47
+ end
46
48
  else
47
- @callbacks[@node.element_name].call if @callbacks[@node.element_name]
49
+ super
48
50
  end
49
51
  end
50
52
 
51
- def success(&callback)
52
- @callbacks['success'] = callback
53
- end
54
-
55
- def failure(&callback)
56
- @callbacks['failure'] = callback
53
+ protected
54
+ def failure(node = nil)
55
+ @failure.call SASLError.import(node)
57
56
  end
58
57
 
59
- protected
58
+ ##
59
+ # Base64 Encoder
60
60
  def b64(str)
61
61
  [str].pack('m').gsub(/\s/,'')
62
62
  end
63
63
 
64
+ ##
65
+ # Builds a standard auth node
64
66
  def auth_node(mechanism, content = nil)
65
67
  node = XMPPNode.new 'auth', content
66
68
  node['xmlns'] = SASL_NS
@@ -68,16 +70,33 @@ module Stream # :nodoc:
68
70
  node
69
71
  end
70
72
 
71
- module DigestMD5 # :nodoc:
72
- def self.extended(obj)
73
- obj.instance_eval { @callbacks['challenge'] = proc { decode_challenge; respond } }
74
- end
73
+ ##
74
+ # Respond to the <mechanisms> node sent by the server
75
+ def mechanisms
76
+ @mechanisms = @node.children
77
+ authenticate if set_mechanism
78
+ end
75
79
 
80
+ ##
81
+ # Digest MD5 authentication
82
+ module DigestMD5 # :nodoc:
83
+ ##
84
+ # Lets the server know we're going to try DigestMD5 authentication
76
85
  def authenticate
77
86
  @stream.send auth_node('DIGEST-MD5')
78
87
  end
79
88
 
89
+ ##
90
+ # Receive the challenge command.
91
+ def challenge
92
+ decode_challenge
93
+ respond
94
+ end
95
+
80
96
  private
97
+ ##
98
+ # Decodes digest strings 'foo=bar,baz="faz"'
99
+ # into {'foo' => 'bar', 'baz' => 'faz'}
81
100
  def decode_challenge
82
101
  text = @node.content.unpack('m').first
83
102
  res = {}
@@ -92,12 +111,16 @@ module Stream # :nodoc:
92
111
  @realm ||= res['realm']
93
112
  end
94
113
 
114
+ ##
115
+ # Builds the properly encoded challenge response
95
116
  def generate_response
96
117
  a1 = "#{d("#{@response[:username]}:#{@response[:realm]}:#{@pass}")}:#{@response[:nonce]}:#{@response[:cnonce]}"
97
118
  a2 = "AUTHENTICATE:#{@response[:'digest-uri']}"
98
119
  h("#{h(a1)}:#{@response[:nonce]}:#{@response[:nc]}:#{@response[:cnonce]}:#{@response[:qop]}:#{h(a2)}")
99
120
  end
100
121
 
122
+ ##
123
+ # Send challenge response
101
124
  def respond
102
125
  node = XMPPNode.new 'response'
103
126
  node['xmlns'] = SASL_NS
@@ -121,6 +144,7 @@ module Stream # :nodoc:
121
144
  LOG.debug "CH RESP TXT: #{@response.map { |k,v| "#{k}=#{v}" } * ','}"
122
145
 
123
146
  # order is to simplify testing
147
+ # Ruby 1.9 eliminates the need for this with ordered hashes
124
148
  order = [:nonce, :charset, :username, :realm, :cnonce, :nc, :qop, :'digest-uri', :response]
125
149
  node.content = b64(order.map { |k| v = @response[k]; "#{k}=#{v}" } * ',')
126
150
  end
@@ -143,7 +167,6 @@ module Stream # :nodoc:
143
167
  @stream.send auth_node('ANONYMOUS', b64(@jid.node))
144
168
  end
145
169
  end #Anonymous
146
-
147
170
  end #SASL
148
171
 
149
172
  end #Stream
@@ -1,26 +1,15 @@
1
1
  module Blather # :nodoc:
2
- module Stream # :nodoc:
2
+ class Stream # :nodoc:
3
3
 
4
- class Session # :nodoc:
4
+ class Session < StreamHandler # :nodoc:
5
5
  def initialize(stream, to)
6
- @stream = stream
6
+ super stream
7
7
  @to = to
8
- @callbacks = {}
9
- end
10
-
11
- def success(&callback)
12
- @callbacks[:success] = callback
13
- end
14
-
15
- def failure(&callback)
16
- @callbacks[:failure] = callback
17
- end
18
-
19
- def receive(node)
20
- @node = node
21
- __send__(@node.element_name == 'iq' ? @node['type'] : @node.element_name)
22
8
  end
23
9
 
10
+ private
11
+ ##
12
+ # Send a start session command
24
13
  def session
25
14
  response = Stanza::Iq.new :set
26
15
  response.to = @to
@@ -30,12 +19,16 @@ module Stream # :nodoc:
30
19
  @stream.send response
31
20
  end
32
21
 
22
+ ##
23
+ # The server should respond with a <result> node if all is well
33
24
  def result
34
- @callbacks[:success].call(@jid) if @callbacks[:success]
25
+ success
35
26
  end
36
27
 
28
+ ##
29
+ # Server returned an error.
37
30
  def error
38
- @callbacks[:failure].call if @callbacks[:failure]
31
+ failure StanzaError.import(@node)
39
32
  end
40
33
  end
41
34
 
@@ -0,0 +1,39 @@
1
+ module Blather # :nodoc:
2
+ class Stream # :nodoc:
3
+
4
+ class StreamHandler # :nodoc:
5
+ def on_success(&block); @success = block; end
6
+ def on_failure(&block); @failure = block; end
7
+
8
+ def initialize(stream)
9
+ @stream = stream
10
+ end
11
+
12
+ def handle(node)
13
+ @node = node
14
+ method = @node.element_name == 'iq' ? @node['type'] : @node.element_name
15
+ if self.respond_to?(method, true)
16
+ self.__send__ method
17
+ else
18
+ @failure.call UnknownResponse.new(@node)
19
+ end
20
+ end
21
+
22
+ protected
23
+ ##
24
+ # Handle error response from the server
25
+ def error
26
+ failure
27
+ end
28
+
29
+ def success(message_back = nil)
30
+ @success.call message_back
31
+ end
32
+
33
+ def failure(err = nil)
34
+ @failure.call err
35
+ end
36
+ end #StreamHandler
37
+
38
+ end #Stream
39
+ end #Blather
@@ -1,27 +1,31 @@
1
1
  module Blather # :nodoc:
2
- module Stream # :nodoc:
2
+ class Stream # :nodoc:
3
3
 
4
- class TLS # :nodoc:
5
- def initialize(stream)
6
- @stream = stream
7
- @callbacks = {
8
- 'starttls' => proc { @stream.send "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>" },
9
- 'proceed' => proc { @stream.start_tls; @callbacks['success'].call },
10
- 'success' => proc { },
11
- 'failure' => proc { }
12
- }
4
+ # TLS negotiation invovles 3 node types:
5
+ # * starttls -- Server asking for TLS to be started
6
+ # * proceed -- Server saying it's ready for a TLS connection to be started
7
+ # * failure -- Failed TLS negotiation. Failure results in a closed connection.
8
+ # so there's no message to pass back to the tream
9
+ class TLS < StreamHandler # :nodoc:
10
+ private
11
+ ##
12
+ # After receiving <starttls> from the server send one
13
+ # back to let it know we're ready to start TLS
14
+ def starttls
15
+ @stream.send "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"
13
16
  end
14
17
 
15
- def success(&callback)
16
- @callbacks['success'] = callback
18
+ ##
19
+ # Server's ready for TLS, so start it up
20
+ def proceed
21
+ @stream.start_tls
22
+ success
17
23
  end
18
24
 
19
- def failure(&callback)
20
- @callbacks['failure'] = callback
21
- end
22
-
23
- def receive(node)
24
- @callbacks[node.element_name].call if @callbacks[node.element_name]
25
+ ##
26
+ # Negotiations failed
27
+ def failure
28
+ super StreamError::TLSFailure.new
25
29
  end
26
30
  end #TLS
27
31
 
@@ -5,9 +5,11 @@ module Blather
5
5
  # All XML classes subclass XMPPNode
6
6
  # it allows the addition of helpers
7
7
  class XMPPNode < XML::Node
8
+ BASE_NAMES = %w[presence message iq].freeze
9
+
8
10
  @@registrations = {}
9
11
 
10
- class_inheritable_accessor :xmlns,
12
+ class_inheritable_accessor :ns,
11
13
  :name
12
14
 
13
15
  ##
@@ -16,10 +18,10 @@ module Blather
16
18
  # This registers a namespace that is used when looking
17
19
  # up the class name of the object to instantiate when a new
18
20
  # stanza is received
19
- def self.register(name, xmlns = nil)
21
+ def self.register(name, ns = nil)
20
22
  self.name = name.to_s
21
- self.xmlns = xmlns
22
- @@registrations[[self.name, self.xmlns]] = self
23
+ self.ns = ns
24
+ @@registrations[[self.name, self.ns]] = self
23
25
  end
24
26
 
25
27
  ##
@@ -34,7 +36,7 @@ module Blather
34
36
  # of that class and imports all the <tt>node</tt>'s attributes
35
37
  # and children into it.
36
38
  def self.import(node)
37
- klass = class_from_registration(node.element_name, node.xmlns)
39
+ klass = class_from_registration(node.element_name, node.namespace)
38
40
  if klass && klass != self
39
41
  klass.import(node)
40
42
  else
@@ -42,6 +44,72 @@ module Blather
42
44
  end
43
45
  end
44
46
 
47
+ ##
48
+ # Provides an attribute reader helper. Default behavior is to
49
+ # conver the values of the attribute into a symbol. This can
50
+ # be turned off by passing <tt>:to_sym => false</tt>
51
+ #
52
+ # class Node
53
+ # attribute_reader :type
54
+ # attribute_reader :name, :to_sym => false
55
+ # end
56
+ #
57
+ # n = Node.new
58
+ # n.attributes[:type] = 'foo'
59
+ # n.type == :foo
60
+ # n.attributes[:name] = 'bar'
61
+ # n.name == 'bar'
62
+ def self.attribute_reader(*syms)
63
+ opts = syms.last.is_a?(Hash) ? syms.pop : {}
64
+ syms.flatten.each do |sym|
65
+ class_eval(<<-END, __FILE__, __LINE__)
66
+ def #{sym}
67
+ attributes[:#{sym}]#{".to_sym unless attributes[:#{sym}].blank?" unless opts[:to_sym] == false}
68
+ end
69
+ END
70
+ end
71
+ end
72
+
73
+ ##
74
+ # Provides an attribute writer helper.
75
+ #
76
+ # class Node
77
+ # attribute_writer :type
78
+ # end
79
+ #
80
+ # n = Node.new
81
+ # n.type = 'foo'
82
+ # n.attributes[:type] == 'foo'
83
+ def self.attribute_writer(*syms)
84
+ syms.flatten.each do |sym|
85
+ next if sym.is_a?(Hash)
86
+ class_eval(<<-END, __FILE__, __LINE__)
87
+ def #{sym}=(value)
88
+ attributes[:#{sym}] = value
89
+ end
90
+ END
91
+ end
92
+ end
93
+
94
+ ##
95
+ # Provides an attribute accessor helper combining
96
+ # <tt>attribute_reader</tt> and <tt>attribute_writer</tt>
97
+ #
98
+ # class Node
99
+ # attribute_accessor :type
100
+ # attribute_accessor :name, :to_sym => false
101
+ # end
102
+ #
103
+ # n = Node.new
104
+ # n.type = 'foo'
105
+ # n.type == :foo
106
+ # n.name = 'bar'
107
+ # n.name == 'bar'
108
+ def self.attribute_accessor(*syms)
109
+ attribute_reader *syms
110
+ attribute_writer *syms
111
+ end
112
+
45
113
  ##
46
114
  # Automatically sets the namespace registered by the subclass
47
115
  def initialize(name = nil, content = nil)
@@ -49,7 +117,7 @@ module Blather
49
117
  content = content.to_s if content
50
118
 
51
119
  super name.to_s, content
52
- self.xmlns = self.class.xmlns
120
+ self.namespace = self.class.ns unless BASE_NAMES.include?(name.to_s)
53
121
  end
54
122
 
55
123
  ##
@@ -58,19 +126,22 @@ module Blather
58
126
  self.class.import self
59
127
  end
60
128
 
61
- def xmlns=(ns)
62
- attributes['xmlns'] = ns
129
+ def namespace=(ns)
130
+ if ns
131
+ ns = {nil => ns} unless ns.is_a?(Hash)
132
+ ns.each { |n| XML::Namespace.new self, *n }
133
+ end
63
134
  end
64
135
 
65
- def xmlns
66
- attributes['xmlns']
136
+ def namespace(prefix = nil)
137
+ (ns = namespaces.find_by_prefix(prefix)) ? ns.href : nil
67
138
  end
68
139
 
69
140
  ##
70
141
  # Remove a child with the name and (optionally) namespace given
71
142
  def remove_child(name, ns = nil)
72
143
  name = name.to_s
73
- self.each { |n| n.remove! if n.element_name == name && (!ns || n.xmlns == ns) }
144
+ self.detect { |n| n.remove! if n.element_name == name && (!ns || n.namespace == ns) }
74
145
  end
75
146
 
76
147
  ##
@@ -90,7 +161,9 @@ module Blather
90
161
  ##
91
162
  # Create a copy
92
163
  def copy(deep = true)
93
- self.class.new(self.element_name).inherit(self)
164
+ copy = self.class.new.inherit(self)
165
+ copy.element_name = self.element_name
166
+ copy
94
167
  end
95
168
 
96
169
  ##
@@ -104,21 +177,22 @@ module Blather
104
177
  ##
105
178
  # Inherit only <tt>stanza</tt>'s attributes
106
179
  def inherit_attrs(attrs)
107
- attrs.each { |a| self[a.name] = a.value }
180
+ attrs.each { |a| attributes[a.name] = a.value }
108
181
  self
109
182
  end
110
183
 
111
184
  ##
112
- # Turn itself into a string and remove all whitespace between nodes
113
- def to_s
185
+ # Turn itself into an XML string and remove all whitespace between nodes
186
+ def to_xml
114
187
  # TODO: Fix this for HTML nodes (and any other that might require whitespace)
115
- super.gsub(">\n<", '><')
188
+ to_s.gsub(">\n<", '><')
116
189
  end
117
190
 
118
191
  ##
119
192
  # Override #find to work when a node isn't attached to a document
120
193
  def find(what, nslist = nil)
121
- (self.doc ? super(what, nslist) : select { |i| i.element_name == what})
194
+ what = what.to_s
195
+ (self.doc ? super(what, nslist) : select { |i| i.element_name == what })
122
196
  end
123
197
  end #XMPPNode
124
198