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