blather 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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