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
@@ -0,0 +1,253 @@
1
+ module Blather
2
+
3
+ ##
4
+ # Stream Errors
5
+ # RFC3920 Section 9.3 (http://xmpp.org/rfcs/rfc3920.html#streams-error-rules)
6
+ class StreamError < BlatherError
7
+ class_inheritable_accessor :err_name
8
+ @@registrations = {}
9
+
10
+ register :stream_error
11
+
12
+ attr_reader :text, :extras
13
+
14
+ ##
15
+ # Register the handler and type to simplify importing
16
+ def self.register(handler, err_name)
17
+ super handler
18
+ self.err_name = err_name
19
+ @@registrations[err_name] = self
20
+ end
21
+
22
+ ##
23
+ # Retreive an error class from a given name
24
+ def self.class_from_registration(err_name)
25
+ @@registrations[err_name.to_s] || self
26
+ end
27
+
28
+ ##
29
+ # Factory method for instantiating the proper class
30
+ # for the error
31
+ def self.import(node)
32
+ name = node.find_first('descendant::*[name()!="text"]', 'urn:ietf:params:xml:ns:xmpp-streams').element_name
33
+ text = node.find_first 'descendant::text', 'urn:ietf:params:xml:ns:xmpp-streams'
34
+ text = text.content if text
35
+
36
+ extras = node.find("descendant::*[name()!='text' and name()!='#{name}']").map { |n| n }
37
+
38
+ class_from_registration(name).new text, extras
39
+ end
40
+
41
+ ##
42
+ # <tt>text</tt> is the (optional) error message.
43
+ # <tt>extras</tt> should be an array of nodes to attach to the error
44
+ # each extra should be in an application specific namespace
45
+ # see RFC3920 Section 4.7.2 (http://xmpp.org/rfcs/rfc3920.html#rfc.section.4.7.2)
46
+ def initialize(text = nil, extras = [])
47
+ @text = text
48
+ @extras = extras
49
+ end
50
+
51
+ ##
52
+ # XMPP defined error name
53
+ def err_name
54
+ self.class.err_name
55
+ end
56
+
57
+ ##
58
+ # Creates an XML node from the error
59
+ def to_node
60
+ node = XMPPNode.new('stream:error')
61
+
62
+ err = XMPPNode.new(self.err_name)
63
+ err.namespace = 'urn:ietf:params:xml:ns:xmpp-streams'
64
+ node << err
65
+
66
+ if self.text
67
+ text = XMPPNode.new('text')
68
+ text.namespace = 'urn:ietf:params:xml:ns:xmpp-streams'
69
+ text << self.text
70
+ node << text
71
+ end
72
+
73
+ self.extras.each do |extra|
74
+ extra_copy = extra.copy
75
+ extra_copy.namespace = extra.namespace
76
+ node << extra_copy
77
+ end
78
+ node
79
+ end
80
+
81
+ ##
82
+ # Turns the object into XML fit to be sent over the stream
83
+ def to_xml
84
+ to_node.to_s
85
+ end
86
+
87
+ def inspect # :nodoc:
88
+ "Stream Error (#{self.err_name}): #{self.text}"
89
+ end
90
+ alias_method :to_s, :inspect # :nodoc:
91
+
92
+ ##
93
+ # The entity has sent XML that cannot be processed; this error MAY be used instead of the more specific XML-related errors,
94
+ # such as <bad-namespace-prefix/>, <invalid-xml/>, <restricted-xml/>, <unsupported-encoding/>, and <xml-not-well-formed/>,
95
+ # although the more specific errors are preferred.
96
+ class BadFormat < StreamError
97
+ register :stream_bad_format_error, 'bad-format'
98
+ end
99
+
100
+ ##
101
+ # The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires
102
+ # such a prefix (see XML Namespace Names and Prefixes).
103
+ class BadNamespacePrefix < StreamError
104
+ register :stream_bad_namespace_prefix_error, 'bad-namespace-prefix'
105
+ end
106
+
107
+ ##
108
+ # The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the
109
+ # existing stream.
110
+ class Conflict < StreamError
111
+ register :stream_conflict_error, 'conflict'
112
+ end
113
+
114
+ ##
115
+ # The entity has not generated any traffic over the stream for some period of time (configurable according to a local service policy).
116
+ class ConnectionTimeout < StreamError
117
+ register :stream_connection_timeout_error, 'connection-timeout'
118
+ end
119
+
120
+ ##
121
+ # The value of the 'to' attribute provided by the initiating entity in the stream header corresponds to a hostname that is no
122
+ # longer hosted by the server.
123
+ class HostGone < StreamError
124
+ register :stream_host_gone_error, 'host-gone'
125
+ end
126
+
127
+ ##
128
+ # The value of the 'to' attribute provided by the initiating entity in the stream header does not correspond to a hostname that
129
+ # is hosted by the server.
130
+ class HostUnknown < StreamError
131
+ register :stream_host_unknown_error, 'host-unknown'
132
+ end
133
+
134
+ ##
135
+ # a stanza sent between two servers lacks a 'to' or 'from' attribute (or the attribute has no value).
136
+ class ImproperAddressing < StreamError
137
+ register :stream_improper_addressing_error, 'improper-addressing'
138
+ end
139
+
140
+ ##
141
+ # The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream.
142
+ class InternalServerError < StreamError
143
+ register :stream_internal_server_error, 'internal-server-error'
144
+ end
145
+
146
+ ##
147
+ # The JID or hostname provided in a 'from' address does not match an authorized JID or validated domain negotiated between
148
+ # servers via SASL or dialback, or between a client and a server via authentication and resource binding.
149
+ class InvalidFrom < StreamError
150
+ register :stream_invalid_from_error, 'invalid-from'
151
+ end
152
+
153
+ ##
154
+ # The stream ID or dialback ID is invalid or does not match an ID previously provided.
155
+ class InvalidId < StreamError
156
+ register :stream_invalid_id_error, 'invalid-id'
157
+ end
158
+
159
+ ##
160
+ # The streams namespace name is something other than "http://etherx.jabber.org/streams" or the dialback namespace name is something
161
+ # other than "jabber:server:dialback" (see XML Namespace Names and Prefixes).
162
+ class InvalidNamespace < StreamError
163
+ register :stream_invalid_namespace_error, 'invalid-namespace'
164
+ end
165
+
166
+ ##
167
+ # The entity has sent invalid XML over the stream to a server that performs validation (see Validation).
168
+ class InvalidXml < StreamError
169
+ register :stream_invalid_xml_error, 'invalid-xml'
170
+ end
171
+
172
+ ##
173
+ # The entity has attempted to send data before the stream has been authenticated, or otherwise is not authorized to perform an action
174
+ # related to stream negotiation; the receiving entity MUST NOT process the offending stanza before sending the stream error.
175
+ class NotAuthorized < StreamError
176
+ register :stream_not_authorized_error, 'not-authorized'
177
+ end
178
+
179
+ ##
180
+ # The entity has violated some local service policy; the server MAY choose to specify the policy in the <text/> element or an
181
+ # application-specific condition element.
182
+ class PolicyViolation < StreamError
183
+ register :stream_policy_violation_error, 'policy-violation'
184
+ end
185
+
186
+ ##
187
+ # The server is unable to properly connect to a remote entity that is required for authentication or authorization.
188
+ class RemoteConnectionFailed < StreamError
189
+ register :stream_remote_connection_failed_error, 'remote-connection-failed'
190
+ end
191
+
192
+ ##
193
+ # The server lacks the system resources necessary to service the stream.
194
+ class ResourceConstraint < StreamError
195
+ register :stream_resource_constraint_error, 'resource-constraint'
196
+ end
197
+
198
+ ##
199
+ # The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference,
200
+ # or unescaped character (see Restrictions).
201
+ class RestrictedXml < StreamError
202
+ register :stream_restricted_xml_error, 'restricted-xml'
203
+ end
204
+
205
+ ##
206
+ # The server will not provide service to the initiating entity but is redirecting traffic to another host; the server SHOULD
207
+ # specify the alternate hostname or IP address (which MUST be a valid domain identifier) as the XML character data of the
208
+ # <see-other-host/> element.
209
+ class SeeOtherHost < StreamError
210
+ register :stream_see_other_host_error, 'see-other-host'
211
+ end
212
+
213
+ ##
214
+ # The server is being shut down and all active streams are being closed.
215
+ class SystemShutdown < StreamError
216
+ register :stream_system_shutdown_error, 'system-shutdown'
217
+ end
218
+
219
+ ##
220
+ # The error condition is not one of those defined by the other conditions in this list; this error condition SHOULD be used
221
+ # only in conjunction with an application-specific condition.
222
+ class UndefinedCondition < StreamError
223
+ register :stream_undefined_condition_error, 'undefined-condition'
224
+ end
225
+
226
+ ##
227
+ # The initiating entity has encoded the stream in an encoding that is not supported by the server (see Character Encoding).
228
+ class UnsupportedEncoding < StreamError
229
+ register :stream_unsupported_encoding_error, 'unsupported-encoding'
230
+ end
231
+
232
+ ##
233
+ # The initiating entity has sent a first-level child of the stream that is not supported by the server.
234
+ class UnsupportedStanzaType < StreamError
235
+ register :stream_unsupported_stanza_type_error, 'unsupported-stanza-type'
236
+ end
237
+
238
+ ##
239
+ # The value of the 'version' attribute provided by the initiating entity in the stream header specifies a version of XMPP
240
+ # That is not supported by the server; the server MAY specify the version(s) it supports in the <text/> element.
241
+ class UnsupportedVersion < StreamError
242
+ register :stream_unsupported_version_error, 'unsupported-version'
243
+ end
244
+
245
+ ##
246
+ # The initiating entity has sent XML that is not well-formed as defined by [XML].
247
+ class XmlNotWellFormed < StreamError
248
+ register :stream_xml_not_well_formed_error, 'xml-not-well-formed'
249
+ end
250
+
251
+ end #StreamError
252
+
253
+ end #Blather
@@ -5,24 +5,23 @@ module Blather
5
5
  class JID
6
6
  include Comparable
7
7
 
8
- PATTERN = /^(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?$/
9
-
10
- begin
11
- require 'idn'
12
- USE_STRINGPREP = true
13
- rescue LoadError
14
- USE_STRINGPREP = false
15
- end
8
+ PATTERN = /^(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?$/.freeze
16
9
 
10
+ ##
17
11
  # Get the JID's node
18
12
  attr_reader :node
19
13
 
14
+ ##
20
15
  # Get the JID's domain
21
16
  attr_reader :domain
22
17
 
18
+ ##
23
19
  # Get the JID's resource
24
20
  attr_reader :resource
25
21
 
22
+ ##
23
+ # If a JID is passed in just return it.
24
+ # No need to copy out all the values
26
25
  def self.new(node, domain = nil, resource = nil)
27
26
  node.is_a?(JID) ? node : super
28
27
  end
@@ -39,14 +38,8 @@ module Blather
39
38
  @node, @domain, @resource = @node.to_s.scan(PATTERN).first
40
39
  end
41
40
 
42
- if USE_STRINGPREP
43
- @node = IDN::Stringprep.nodeprep(@node) if @node
44
- @domain = IDN::Stringprep.nameprep(@domain) if @domain
45
- @resource = IDN::Stringprep.resourceprep(@resource) if @resource
46
- else
47
- @node.downcase! if @node
48
- @domain.downcase! if @domain
49
- end
41
+ @node.downcase! if @node
42
+ @domain.downcase! if @domain
50
43
 
51
44
  raise ArgumentError, 'Node too long' if (@node || '').length > 1023
52
45
  raise ArgumentError, 'Domain too long' if (@domain || '').length > 1023
@@ -71,6 +71,15 @@ module Blather
71
71
  @items.dup
72
72
  end
73
73
 
74
+ ##
75
+ # A hash of items keyed by group
76
+ def grouped
77
+ self.inject(Hash.new{|h,k|h[k]=[]}) do |hash, item|
78
+ item[1].groups.each { |group| hash[group] << item[1] }
79
+ hash
80
+ end
81
+ end
82
+
74
83
  private
75
84
  def self.key(jid)
76
85
  JID.new(jid).stripped.to_s
@@ -17,6 +17,7 @@ module Blather
17
17
  # item:: can be a JID, String (a@b) or a Stanza
18
18
  def initialize(item)
19
19
  @statuses = []
20
+ @groups = []
20
21
 
21
22
  case item
22
23
  when JID
@@ -24,12 +25,14 @@ module Blather
24
25
  when String
25
26
  self.jid = JID.new(item).stripped
26
27
  when XMPPNode
27
- self.jid = JID.new(item['jid']).stripped
28
- self.name = item['name']
29
- self.subscription = item['subscription']
30
- self.ask = item['ask']
28
+ self.jid = JID.new(item[:jid]).stripped
29
+ self.name = item[:name]
30
+ self.subscription = item[:subscription]
31
+ self.ask = item[:ask]
31
32
  item.groups.each { |g| self.groups << g }
32
33
  end
34
+
35
+ @groups = [nil] if @groups.empty?
33
36
  end
34
37
 
35
38
  ##
@@ -3,6 +3,7 @@ module Blather
3
3
  # Base XMPP Stanza
4
4
  class Stanza < XMPPNode
5
5
  @@last_id = 0
6
+ @@handler_list = []
6
7
 
7
8
  class_inheritable_array :handler_heirarchy
8
9
 
@@ -13,12 +14,17 @@ module Blather
13
14
  # that inherits Stanza can register a callback for itself
14
15
  # which is added to a list and iterated over when looking for
15
16
  # a callback to use
16
- def self.register(type, name = nil, xmlns = nil)
17
+ def self.register(type, name = nil, ns = nil)
18
+ @@handler_list << type
17
19
  self.handler_heirarchy ||= []
18
20
  self.handler_heirarchy.unshift type
19
21
 
20
22
  name = name || self.name || type
21
- super name, xmlns
23
+ super name, ns
24
+ end
25
+
26
+ def self.handler_list
27
+ @@handler_list
22
28
  end
23
29
 
24
30
  ##
@@ -65,43 +71,31 @@ module Blather
65
71
  self
66
72
  end
67
73
 
68
- def id=(id)
69
- attributes['id'] = id
70
- end
71
-
72
- def id
73
- attributes['id']
74
- end
74
+ attribute_accessor :id, :to_sym => false
75
75
 
76
- def to=(to)
77
- attributes['to'] = to
78
- end
76
+ attribute_writer :to, :from
79
77
 
80
78
  ##
81
79
  # returns:: JID created from the "to" value of the stanza
82
80
  def to
83
- JID.new(attributes['to']) if attributes['to']
84
- end
85
-
86
- def from=(from)
87
- attributes['from'] = from
81
+ JID.new(attributes[:to]) if attributes[:to]
88
82
  end
89
83
 
90
84
  ##
91
85
  # returns:: JID created from the "from" value of the stanza
92
86
  def from
93
- JID.new(attributes['from']) if attributes['from']
87
+ JID.new(attributes[:from]) if attributes[:from]
94
88
  end
95
89
 
96
- def type=(type)
97
- attributes['type'] = type
98
- end
90
+ attribute_accessor :type
99
91
 
100
92
  ##
101
- # returns:: a symbol of the type
102
- def type
103
- attributes['type'].to_sym unless attributes['type'].blank?
93
+ # Transform the stanza into a stanza error
94
+ # <tt>err_name_or_class</tt> can be the name of the error or the error class to use
95
+ # <tt>type</tt>, <tt>text</tt>, <tt>extras</tt> are the same as for StanzaError#new
96
+ def as_error(err_name_or_class, type, text = nil, extras = [])
97
+ klass = (err_name_or_class.is_a?(Class) ? err_name_or_class : StanzaError.class_from_registration(err_name_or_class))
98
+ klass.new self, type, text, extras
104
99
  end
105
-
106
100
  end
107
101
  end
@@ -0,0 +1,9 @@
1
+ module Blather
2
+ class Stanza
3
+
4
+ class Disco < Iq::Query
5
+ attribute_accessor :node, :to_sym => false
6
+ end
7
+
8
+ end #Stanza
9
+ end #Blather
@@ -0,0 +1,84 @@
1
+ module Blather
2
+ class Stanza
3
+
4
+ ##
5
+ # DiscoInfo object ()
6
+ #
7
+ class DiscoInfo < Disco
8
+ register :disco_info, nil, 'http://jabber.org/protocol/disco#info'
9
+
10
+ def initialize(type = nil, node = nil, identities = [], features = [])
11
+ super type
12
+
13
+ self.node = node
14
+
15
+ [identities].flatten.each do |id|
16
+ query << (id.is_a?(Identity) ? id : Identity.new(id[:name], id[:type], id[:category]))
17
+ end
18
+
19
+ [features].flatten.each do |feature|
20
+ query << (feature.is_a?(Feature) ? feature : Feature.new(feature))
21
+ end
22
+ end
23
+
24
+ ##
25
+ # List of identity objects
26
+ def identities
27
+ identities = query.find('identity')
28
+ identities = query.find('query_ns:identity', :query_ns => self.class.ns) if identities.empty?
29
+ identities.map { |i| Identity.new i }
30
+ end
31
+
32
+ ##
33
+ # List of feature objects
34
+ def features
35
+ features = query.find('feature')
36
+ features = query.find('query_ns:feature', :query_ns => self.class.ns) if features.empty?
37
+ features.map { |i| Feature.new i }
38
+ end
39
+
40
+ class Identity < XMPPNode
41
+ attribute_accessor :category, :type
42
+ attribute_accessor :name, :to_sym => false
43
+
44
+ def initialize(name, type = nil, category = nil)
45
+ super :identity
46
+
47
+ if name.is_a?(XML::Node)
48
+ self.inherit name
49
+ else
50
+ self.name = name
51
+ self.type = type
52
+ self.category = category
53
+ end
54
+ end
55
+
56
+ def eql?(other)
57
+ other.kind_of?(self.class) &&
58
+ other.name == self.name &&
59
+ other.type == self.type &&
60
+ other.category == self.category
61
+ end
62
+ end
63
+
64
+ class Feature < XMPPNode
65
+ attribute_accessor :var, :to_sym => false
66
+
67
+ def initialize(var)
68
+ super :feature
69
+ if var.is_a?(XML::Node)
70
+ self.inherit var
71
+ else
72
+ self.var = var
73
+ end
74
+ end
75
+
76
+ def eql?(other)
77
+ other.kind_of?(self.class) &&
78
+ other.var == self.var
79
+ end
80
+ end
81
+ end
82
+
83
+ end #Stanza
84
+ end #Blather