blather 0.1
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/CHANGELOG +1 -0
- data/LICENSE +20 -0
- data/Manifest +43 -0
- data/README.rdoc +78 -0
- data/Rakefile +16 -0
- data/blather.gemspec +41 -0
- data/examples/echo.rb +22 -0
- data/examples/shell_client.rb +28 -0
- data/lib/autotest/discover.rb +1 -0
- data/lib/autotest/spec.rb +60 -0
- data/lib/blather.rb +46 -0
- data/lib/blather/callback.rb +24 -0
- data/lib/blather/client.rb +81 -0
- data/lib/blather/core/errors.rb +24 -0
- data/lib/blather/core/jid.rb +101 -0
- data/lib/blather/core/roster.rb +84 -0
- data/lib/blather/core/roster_item.rb +92 -0
- data/lib/blather/core/stanza.rb +116 -0
- data/lib/blather/core/stanza/iq.rb +27 -0
- data/lib/blather/core/stanza/iq/query.rb +42 -0
- data/lib/blather/core/stanza/iq/roster.rb +96 -0
- data/lib/blather/core/stanza/message.rb +55 -0
- data/lib/blather/core/stanza/presence.rb +35 -0
- data/lib/blather/core/stanza/presence/status.rb +77 -0
- data/lib/blather/core/stanza/presence/subscription.rb +73 -0
- data/lib/blather/core/stream.rb +181 -0
- data/lib/blather/core/stream/parser.rb +74 -0
- data/lib/blather/core/stream/resource.rb +51 -0
- data/lib/blather/core/stream/sasl.rb +135 -0
- data/lib/blather/core/stream/session.rb +43 -0
- data/lib/blather/core/stream/tls.rb +29 -0
- data/lib/blather/core/sugar.rb +150 -0
- data/lib/blather/core/xmpp_node.rb +132 -0
- data/lib/blather/extensions.rb +4 -0
- data/lib/blather/extensions/last_activity.rb +55 -0
- data/lib/blather/extensions/version.rb +85 -0
- data/spec/blather/core/jid_spec.rb +78 -0
- data/spec/blather/core/roster_item_spec.rb +80 -0
- data/spec/blather/core/roster_spec.rb +79 -0
- data/spec/blather/core/stanza_spec.rb +95 -0
- data/spec/blather/core/stream_spec.rb +263 -0
- data/spec/blather/core/xmpp_node_spec.rb +130 -0
- data/spec/build_safe.rb +20 -0
- data/spec/spec_helper.rb +49 -0
- metadata +172 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
module Blather # :nodoc:
|
2
|
+
module Stream # :nodoc:
|
3
|
+
|
4
|
+
class Parser # :nodoc:
|
5
|
+
STREAM_REGEX = %r{(/)?stream:stream}.freeze
|
6
|
+
|
7
|
+
@@debug = false
|
8
|
+
def self.debug; @@debug; end
|
9
|
+
def self.debug=(debug); @@debug = debug; end
|
10
|
+
|
11
|
+
include XML::SaxParser::Callbacks
|
12
|
+
|
13
|
+
def initialize(receiver)
|
14
|
+
@receiver = receiver
|
15
|
+
@current = nil
|
16
|
+
|
17
|
+
@parser = XML::SaxParser.new
|
18
|
+
@parser.callbacks = self
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse(string)
|
22
|
+
LOG.debug "PARSING: #{string}" if @@debug
|
23
|
+
if string =~ STREAM_REGEX && $1
|
24
|
+
@receiver.receive XMPPNode.new('stream:end')
|
25
|
+
else
|
26
|
+
string << "</stream:stream>" if string =~ STREAM_REGEX && !$1
|
27
|
+
|
28
|
+
@parser.string = string
|
29
|
+
@parser.parse
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_start_element(elem, attrs)
|
34
|
+
LOG.debug "START ELEM: (#{[elem, attrs].inspect})" if @@debug
|
35
|
+
e = XMPPNode.new elem
|
36
|
+
attrs.each { |n,v| e[n] = v }
|
37
|
+
|
38
|
+
if elem == 'stream:stream'
|
39
|
+
@receiver.receive e
|
40
|
+
|
41
|
+
elsif !@receiver.stopped?
|
42
|
+
@current << e if @current
|
43
|
+
@current = e
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def on_characters(chars = '')
|
49
|
+
LOG.debug "CHARS: #{chars}" if @@debug
|
50
|
+
@current << XML::Node.new_text(chars) if @current
|
51
|
+
end
|
52
|
+
|
53
|
+
def on_cdata_block(block)
|
54
|
+
LOG.debug "CDATA: #{block}" if @@debug
|
55
|
+
@current << XML::Node.new_cdata(block) if @current
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_end_element(elem)
|
59
|
+
return if elem =~ STREAM_REGEX
|
60
|
+
|
61
|
+
LOG.debug "END ELEM: (#{@current}) #{elem}" if @@debug
|
62
|
+
if @current.parent?
|
63
|
+
@current = @current.parent
|
64
|
+
|
65
|
+
else
|
66
|
+
c, @current = @current, nil
|
67
|
+
@receiver.receive c
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end #Parser
|
72
|
+
|
73
|
+
end #Stream
|
74
|
+
end #Blather
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Blather # :nodoc:
|
2
|
+
module Stream # :nodoc:
|
3
|
+
|
4
|
+
class Resource # :nodoc:
|
5
|
+
def initialize(stream, jid)
|
6
|
+
@stream = stream
|
7
|
+
@jid = jid
|
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
|
+
end
|
23
|
+
|
24
|
+
def bind
|
25
|
+
binder = XMPPNode.new('bind')
|
26
|
+
binder.xmlns = 'urn:ietf:params:xml:ns:xmpp-bind'
|
27
|
+
|
28
|
+
binder << XMPPNode.new('resource', @jid.resource) if @jid.resource
|
29
|
+
|
30
|
+
response = Stanza::Iq.new :set
|
31
|
+
@id = response.id
|
32
|
+
response << binder
|
33
|
+
|
34
|
+
@stream.send response
|
35
|
+
end
|
36
|
+
|
37
|
+
def result
|
38
|
+
LOG.debug "RESOURE NODE #{@node}"
|
39
|
+
if @id == @node['id']
|
40
|
+
@jid = JID.new @node.find_first('bind').content_from(:jid)
|
41
|
+
@callbacks[:success].call(@jid) if @callbacks[:success]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def error
|
46
|
+
@callbacks[:failure].call if @callbacks[:failure]
|
47
|
+
end
|
48
|
+
end #Resource
|
49
|
+
|
50
|
+
end #Stream
|
51
|
+
end #Blather
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module Blather # :nodoc:
|
2
|
+
module Stream # :nodoc:
|
3
|
+
|
4
|
+
class SASL # :nodoc:
|
5
|
+
SASL_NS = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
6
|
+
|
7
|
+
def initialize(stream, jid, pass = nil)
|
8
|
+
@stream = stream
|
9
|
+
@jid = jid
|
10
|
+
@pass = pass
|
11
|
+
@callbacks = {}
|
12
|
+
|
13
|
+
init_callbacks
|
14
|
+
end
|
15
|
+
|
16
|
+
def init_callbacks
|
17
|
+
@callbacks['mechanisms'] = proc { set_mechanism; authenticate }
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_mechanism
|
21
|
+
mod = case (mechanism = @node.first.content)
|
22
|
+
when 'DIGEST-MD5' then DigestMD5
|
23
|
+
when 'PLAIN' then Plain
|
24
|
+
when 'ANONYMOUS' then Anonymous
|
25
|
+
else raise "Unknown SASL mechanism (#{mechanism})"
|
26
|
+
end
|
27
|
+
|
28
|
+
extend mod
|
29
|
+
end
|
30
|
+
|
31
|
+
def receive(node)
|
32
|
+
@node = node
|
33
|
+
@callbacks[@node.element_name].call if @callbacks[@node.element_name]
|
34
|
+
end
|
35
|
+
|
36
|
+
def success(&callback)
|
37
|
+
@callbacks['success'] = callback
|
38
|
+
end
|
39
|
+
|
40
|
+
def failure(&callback)
|
41
|
+
@callbacks['failure'] = callback
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
def b64(str)
|
46
|
+
[str].pack('m').gsub(/\s/,'')
|
47
|
+
end
|
48
|
+
|
49
|
+
def auth_node(mechanism, content = nil)
|
50
|
+
node = XMPPNode.new 'auth', content
|
51
|
+
node['xmlns'] = SASL_NS
|
52
|
+
node['mechanism'] = mechanism
|
53
|
+
node
|
54
|
+
end
|
55
|
+
|
56
|
+
module DigestMD5 # :nodoc:
|
57
|
+
def self.extended(obj)
|
58
|
+
obj.instance_eval { @callbacks['challenge'] = proc { decode_challenge; respond } }
|
59
|
+
end
|
60
|
+
|
61
|
+
def authenticate
|
62
|
+
@stream.send auth_node('DIGEST-MD5')
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def decode_challenge
|
67
|
+
text = @node.content.unpack('m').first
|
68
|
+
res = {}
|
69
|
+
|
70
|
+
text.split(',').each do |statement|
|
71
|
+
key, value = statement.split('=')
|
72
|
+
res[key] = value.delete('"') unless key.empty?
|
73
|
+
end
|
74
|
+
LOG.debug "CHALLENGE DECODE: #{res.inspect}"
|
75
|
+
|
76
|
+
@nonce ||= res['nonce']
|
77
|
+
@realm ||= res['realm']
|
78
|
+
end
|
79
|
+
|
80
|
+
def generate_response
|
81
|
+
a1 = "#{d("#{@response[:username]}:#{@response[:realm]}:#{@pass}")}:#{@response[:nonce]}:#{@response[:cnonce]}"
|
82
|
+
a2 = "AUTHENTICATE:#{@response[:'digest-uri']}"
|
83
|
+
h("#{h(a1)}:#{@response[:nonce]}:#{@response[:nc]}:#{@response[:cnonce]}:#{@response[:qop]}:#{h(a2)}")
|
84
|
+
end
|
85
|
+
|
86
|
+
def respond
|
87
|
+
node = XMPPNode.new 'response'
|
88
|
+
node['xmlns'] = SASL_NS
|
89
|
+
|
90
|
+
unless @initial_response_sent
|
91
|
+
@initial_response_sent = true
|
92
|
+
@response = {
|
93
|
+
:nonce => @nonce,
|
94
|
+
:charset => 'utf-8',
|
95
|
+
:username => @jid.node,
|
96
|
+
:realm => @realm || @jid.domain,
|
97
|
+
:cnonce => h(Time.new.to_f.to_s),
|
98
|
+
:nc => '00000001',
|
99
|
+
:qop => 'auth',
|
100
|
+
:'digest-uri' => "xmpp/#{@jid.domain}",
|
101
|
+
}
|
102
|
+
@response[:response] = generate_response
|
103
|
+
@response.each { |k,v| @response[k] = "\"#{v}\"" unless [:nc, :qop, :response, :charset].include?(k) }
|
104
|
+
|
105
|
+
LOG.debug "CHALLENGE RESPOSNE: #{@response.inspect}"
|
106
|
+
LOG.debug "CH RESP TXT: #{@response.map { |k,v| "#{k}=#{v}" } * ','}"
|
107
|
+
|
108
|
+
# order is to simplify testing
|
109
|
+
order = [:nonce, :charset, :username, :realm, :cnonce, :nc, :qop, :'digest-uri']
|
110
|
+
node.content = b64(order.map { |k| v = @response[k]; "#{k}=#{v}" } * ',')
|
111
|
+
end
|
112
|
+
|
113
|
+
@stream.send node
|
114
|
+
end
|
115
|
+
|
116
|
+
def d(s); Digest::MD5.digest(s); end
|
117
|
+
def h(s); Digest::MD5.hexdigest(s); end
|
118
|
+
end #DigestMD5
|
119
|
+
|
120
|
+
module Plain # :nodoc:
|
121
|
+
def authenticate
|
122
|
+
@stream.send auth_node('PLAIN', b64("#{@jid.stripped}\x00#{@jid.node}\x00#{@pass}"))
|
123
|
+
end
|
124
|
+
end #Plain
|
125
|
+
|
126
|
+
module Anonymous # :nodoc:
|
127
|
+
def authenticate
|
128
|
+
@stream.send auth_node('ANONYMOUS', b64(@jid.node))
|
129
|
+
end
|
130
|
+
end #Anonymous
|
131
|
+
|
132
|
+
end #SASL
|
133
|
+
|
134
|
+
end #Stream
|
135
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Blather # :nodoc:
|
2
|
+
module Stream # :nodoc:
|
3
|
+
|
4
|
+
class Session # :nodoc:
|
5
|
+
def initialize(stream, to)
|
6
|
+
@stream = stream
|
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
|
+
end
|
23
|
+
|
24
|
+
def session
|
25
|
+
response = Stanza::Iq.new :set
|
26
|
+
response.to = @to
|
27
|
+
sess = XMPPNode.new 'session'
|
28
|
+
sess['xmlns'] = 'urn:ietf:params:xml:ns:xmpp-session'
|
29
|
+
response << sess
|
30
|
+
@stream.send response
|
31
|
+
end
|
32
|
+
|
33
|
+
def result
|
34
|
+
@callbacks[:success].call(@jid) if @callbacks[:success]
|
35
|
+
end
|
36
|
+
|
37
|
+
def error
|
38
|
+
@callbacks[:failure].call if @callbacks[:failure]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Blather # :nodoc:
|
2
|
+
module Stream # :nodoc:
|
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
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def success(&callback)
|
16
|
+
@callbacks['success'] = callback
|
17
|
+
end
|
18
|
+
|
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
|
+
end
|
26
|
+
end #TLS
|
27
|
+
|
28
|
+
end #Stream
|
29
|
+
end #Blather
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module LibXML # :nodoc:
|
2
|
+
module XML # :nodoc:
|
3
|
+
|
4
|
+
class Attributes
|
5
|
+
# Helper method for removing attributes
|
6
|
+
def remove(name)
|
7
|
+
name = name.to_s
|
8
|
+
self.each { |a| a.remove! or break if a.name == name }
|
9
|
+
end
|
10
|
+
end #Attributes
|
11
|
+
|
12
|
+
end #XML
|
13
|
+
end #LibXML
|
14
|
+
|
15
|
+
## Thanks to ActiveSupport for everything below this line
|
16
|
+
class Class # :nodoc:
|
17
|
+
def class_inheritable_reader(*syms)
|
18
|
+
syms.each do |sym|
|
19
|
+
next if sym.is_a?(Hash)
|
20
|
+
class_eval <<-EOS
|
21
|
+
def self.#{sym}
|
22
|
+
read_inheritable_attribute(:#{sym})
|
23
|
+
end
|
24
|
+
|
25
|
+
def #{sym}
|
26
|
+
self.class.#{sym}
|
27
|
+
end
|
28
|
+
EOS
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def class_inheritable_writer(*syms)
|
33
|
+
syms.each do |sym|
|
34
|
+
class_eval <<-EOS
|
35
|
+
def self.#{sym}=(obj)
|
36
|
+
write_inheritable_attribute(:#{sym}, obj)
|
37
|
+
end
|
38
|
+
EOS
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def class_inheritable_array_writer(*syms)
|
43
|
+
syms.each do |sym|
|
44
|
+
class_eval <<-EOS
|
45
|
+
def self.#{sym}=(obj)
|
46
|
+
write_inheritable_array(:#{sym}, obj)
|
47
|
+
end
|
48
|
+
EOS
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def class_inheritable_hash_writer(*syms)
|
53
|
+
syms.each do |sym|
|
54
|
+
class_eval <<-EOS
|
55
|
+
def self.#{sym}=(obj)
|
56
|
+
write_inheritable_hash(:#{sym}, obj)
|
57
|
+
end
|
58
|
+
EOS
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def class_inheritable_accessor(*syms)
|
63
|
+
class_inheritable_reader(*syms)
|
64
|
+
class_inheritable_writer(*syms)
|
65
|
+
end
|
66
|
+
|
67
|
+
def class_inheritable_array(*syms)
|
68
|
+
class_inheritable_reader(*syms)
|
69
|
+
class_inheritable_array_writer(*syms)
|
70
|
+
end
|
71
|
+
|
72
|
+
def class_inheritable_hash(*syms)
|
73
|
+
class_inheritable_reader(*syms)
|
74
|
+
class_inheritable_hash_writer(*syms)
|
75
|
+
end
|
76
|
+
|
77
|
+
def inheritable_attributes
|
78
|
+
@inheritable_attributes ||= EMPTY_INHERITABLE_ATTRIBUTES
|
79
|
+
end
|
80
|
+
|
81
|
+
def write_inheritable_attribute(key, value)
|
82
|
+
if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
|
83
|
+
@inheritable_attributes = {}
|
84
|
+
end
|
85
|
+
inheritable_attributes[key] = value
|
86
|
+
end
|
87
|
+
|
88
|
+
def write_inheritable_array(key, elements)
|
89
|
+
write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
|
90
|
+
write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
|
91
|
+
end
|
92
|
+
|
93
|
+
def write_inheritable_hash(key, hash)
|
94
|
+
write_inheritable_attribute(key, {}) if read_inheritable_attribute(key).nil?
|
95
|
+
write_inheritable_attribute(key, read_inheritable_attribute(key).merge(hash))
|
96
|
+
end
|
97
|
+
|
98
|
+
def read_inheritable_attribute(key)
|
99
|
+
inheritable_attributes[key]
|
100
|
+
end
|
101
|
+
|
102
|
+
def reset_inheritable_attributes
|
103
|
+
@inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
# Prevent this constant from being created multiple times
|
108
|
+
EMPTY_INHERITABLE_ATTRIBUTES = {}.freeze unless const_defined?(:EMPTY_INHERITABLE_ATTRIBUTES)
|
109
|
+
|
110
|
+
def inherited_with_inheritable_attributes(child)
|
111
|
+
inherited_without_inheritable_attributes(child) if respond_to?(:inherited_without_inheritable_attributes)
|
112
|
+
|
113
|
+
if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
|
114
|
+
new_inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
|
115
|
+
else
|
116
|
+
new_inheritable_attributes = inheritable_attributes.inject({}) do |memo, (key, value)|
|
117
|
+
memo.update(key => value.duplicable? ? value.dup : value)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
child.instance_variable_set('@inheritable_attributes', new_inheritable_attributes)
|
122
|
+
end
|
123
|
+
|
124
|
+
alias inherited_without_inheritable_attributes inherited
|
125
|
+
alias inherited inherited_with_inheritable_attributes
|
126
|
+
end #Class
|
127
|
+
|
128
|
+
class Object # :nodoc:
|
129
|
+
def duplicable?; true; end
|
130
|
+
end
|
131
|
+
|
132
|
+
class NilClass #:nodoc:
|
133
|
+
def duplicable?; false; end
|
134
|
+
end
|
135
|
+
|
136
|
+
class FalseClass #:nodoc:
|
137
|
+
def duplicable?; false; end
|
138
|
+
end
|
139
|
+
|
140
|
+
class TrueClass #:nodoc:
|
141
|
+
def duplicable?; false; end
|
142
|
+
end
|
143
|
+
|
144
|
+
class Symbol #:nodoc:
|
145
|
+
def duplicable?; false; end
|
146
|
+
end
|
147
|
+
|
148
|
+
class Numeric #:nodoc:
|
149
|
+
def duplicable?; false; end
|
150
|
+
end
|