em-xmpp 0.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/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/em-xmpp.gemspec +20 -0
- data/lib/em-xmpp/cert_store.rb +76 -0
- data/lib/em-xmpp/connection.rb +116 -0
- data/lib/em-xmpp/connector.rb +244 -0
- data/lib/em-xmpp/context.rb +249 -0
- data/lib/em-xmpp/handler.rb +262 -0
- data/lib/em-xmpp/jid.rb +27 -0
- data/lib/em-xmpp/namespaces.rb +31 -0
- data/lib/em-xmpp/nodes.rb +8 -0
- data/lib/em-xmpp/resolver.rb +23 -0
- data/lib/em-xmpp/version.rb +5 -0
- data/lib/em-xmpp.rb +9 -0
- data/samples/echo.rb +40 -0
- metadata +96 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2012 crapooze
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Em::Xmpp
|
|
2
|
+
|
|
3
|
+
TODO: Write a gem description
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
gem 'em-xmpp'
|
|
10
|
+
|
|
11
|
+
And then execute:
|
|
12
|
+
|
|
13
|
+
$ bundle
|
|
14
|
+
|
|
15
|
+
Or install it yourself as:
|
|
16
|
+
|
|
17
|
+
$ gem install em-xmpp
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
TODO: Write usage instructions here
|
|
22
|
+
|
|
23
|
+
## Contributing
|
|
24
|
+
|
|
25
|
+
1. Fork it
|
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/em-xmpp.gemspec
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
require File.expand_path('../lib/em-xmpp/version', __FILE__)
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |gem|
|
|
5
|
+
gem.authors = ["crapooze"]
|
|
6
|
+
gem.email = ["crapooze@gmail.com"]
|
|
7
|
+
gem.description = %q{XMPP client for event machine}
|
|
8
|
+
gem.summary = %q{Easy to write and to extend XMPP client}
|
|
9
|
+
gem.homepage = "https://github.com/crapooze/em-xmpp"
|
|
10
|
+
|
|
11
|
+
gem.files = `git ls-files`.split($\)
|
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
14
|
+
gem.name = "em-xmpp"
|
|
15
|
+
gem.require_paths = ["lib"]
|
|
16
|
+
gem.version = Em::Xmpp::VERSION
|
|
17
|
+
gem.add_dependency "eventmachine"
|
|
18
|
+
gem.add_dependency "nokogiri"
|
|
19
|
+
gem.add_dependency "ruby-sasl"
|
|
20
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
The code from this file was copied from Blather:
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2011 Jeff Smick
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
8
|
+
a copy of this software and associated documentation files (the
|
|
9
|
+
"Software"), to deal in the Software without restriction, including
|
|
10
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
11
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
12
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
13
|
+
the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be
|
|
16
|
+
included in all copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
20
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
21
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
22
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
23
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
24
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
25
|
+
=end
|
|
26
|
+
|
|
27
|
+
module EM::Xmpp
|
|
28
|
+
# An X509 certificate store that validates certificate trust chains.
|
|
29
|
+
# This uses the #{cert_directory}/*.crt files as the list of trusted root
|
|
30
|
+
# CA certificates.
|
|
31
|
+
class CertStore
|
|
32
|
+
@@certs = nil
|
|
33
|
+
@cert_directory = nil
|
|
34
|
+
|
|
35
|
+
def initialize(cert_directory)
|
|
36
|
+
@cert_directory = cert_directory
|
|
37
|
+
@store = OpenSSL::X509::Store.new
|
|
38
|
+
certs.each {|c| @store.add_cert(c) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Return true if the certificate is signed by a CA certificate in the
|
|
42
|
+
# store. If the certificate can be trusted, it's added to the store so
|
|
43
|
+
# it can be used to trust other certs.
|
|
44
|
+
def trusted?(pem)
|
|
45
|
+
if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
|
|
46
|
+
@store.verify(cert).tap do |trusted|
|
|
47
|
+
@store.add_cert(cert) if trusted rescue nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Return true if the domain name matches one of the names in the
|
|
53
|
+
# certificate. In other words, is the certificate provided to us really
|
|
54
|
+
# for the domain to which we think we're connected?
|
|
55
|
+
def domain?(pem, domain)
|
|
56
|
+
if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
|
|
57
|
+
OpenSSL::SSL.verify_certificate_identity(cert, domain) rescue false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Return the trusted root CA certificates installed in the @cert_directory. These
|
|
62
|
+
# certificates are used to start the trust chain needed to validate certs
|
|
63
|
+
# we receive from clients and servers.
|
|
64
|
+
def certs
|
|
65
|
+
unless @@certs
|
|
66
|
+
pattern = /-{5}BEGIN CERTIFICATE-{5}\n.*?-{5}END CERTIFICATE-{5}\n/m
|
|
67
|
+
dir = @cert_directory
|
|
68
|
+
certs = Dir[File.join(dir, '*.crt')].map {|f| File.read(f) }
|
|
69
|
+
certs = certs.map {|c| c.scan(pattern) }.flatten
|
|
70
|
+
certs.map! {|c| OpenSSL::X509::Certificate.new(c) }
|
|
71
|
+
@@certs = certs.reject {|c| c.not_after < Time.now }
|
|
72
|
+
end
|
|
73
|
+
@@certs
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
|
|
2
|
+
require 'em-xmpp/namespaces'
|
|
3
|
+
require 'em-xmpp/connector'
|
|
4
|
+
require 'em-xmpp/handler'
|
|
5
|
+
require 'em-xmpp/jid'
|
|
6
|
+
require 'em-xmpp/cert_store'
|
|
7
|
+
require 'eventmachine'
|
|
8
|
+
|
|
9
|
+
module EM::Xmpp
|
|
10
|
+
class Connection < EM::Connection
|
|
11
|
+
include Namespaces
|
|
12
|
+
include Connector
|
|
13
|
+
|
|
14
|
+
attr_reader :jid, :pass
|
|
15
|
+
|
|
16
|
+
def initialize(jid, pass, mod=nil, cfg={})
|
|
17
|
+
@jid = jid
|
|
18
|
+
@pass = pass.dup.freeze
|
|
19
|
+
self.extend mod if mod
|
|
20
|
+
certdir = cfg[:certificates]
|
|
21
|
+
@certstore = if certdir
|
|
22
|
+
CertStore.new(certdir)
|
|
23
|
+
else
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def post_init
|
|
29
|
+
super
|
|
30
|
+
@handler = StreamNegotiation.new self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stanza_start(node)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def stanza_end(node)
|
|
37
|
+
@handler.handle(node)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def unhandled_stanza(node)
|
|
41
|
+
raise RuntimeError, "did not handle node:\n#{node}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def jid_received(jid)
|
|
45
|
+
@jid = JID.parse jid
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def negotiation_finished
|
|
49
|
+
@pass = nil
|
|
50
|
+
@handler = Routine.new self
|
|
51
|
+
send_raw presence_stanza()
|
|
52
|
+
ready
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def negotiation_failed(node)
|
|
56
|
+
raise RuntimeError, "could not negotiate a stream:\n#{node}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
#should add 'xml:lang'
|
|
60
|
+
|
|
61
|
+
def default_presence_config
|
|
62
|
+
{}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def default_message_config
|
|
66
|
+
{'to' => @jid.domain, 'id' => "em-xmpp.#{rand(65535)}"}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def default_iq_config
|
|
70
|
+
{'type' => 'get', 'id' => "em-xmpp.#{rand(65535)}"}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def presence_stanza(cfg={}, &blk)
|
|
74
|
+
cfg = default_presence_config.merge(cfg)
|
|
75
|
+
build_xml do |x|
|
|
76
|
+
x.presence(cfg, &blk)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def message_stanza(cfg={}, &blk)
|
|
81
|
+
cfg = default_message_config.merge(cfg)
|
|
82
|
+
build_xml do |x|
|
|
83
|
+
x.message(cfg, &blk)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def iq_stanza(cfg={}, &blk)
|
|
88
|
+
cfg = default_iq_config.merge(cfg)
|
|
89
|
+
build_xml do |x|
|
|
90
|
+
x.iq(cfg, &blk)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
%w{on on_exception on_presence on_iq on_message}.each do |meth|
|
|
95
|
+
define_method(meth) do |*args,&blk|
|
|
96
|
+
@handler.send meth, *args, &blk
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def ready
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def start_using_tls_and_reset_stream
|
|
104
|
+
bool = !! @certstore
|
|
105
|
+
start_tls(:verify_peer => bool)
|
|
106
|
+
restart_xml_stream
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def ssl_verify_peer(pem)
|
|
110
|
+
@certstore.trusted?(pem).tap do |trusted|
|
|
111
|
+
close_connection unless trusted
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
|
|
2
|
+
require 'nokogiri'
|
|
3
|
+
require 'eventmachine'
|
|
4
|
+
require 'em-xmpp/context'
|
|
5
|
+
require 'em-xmpp/namespaces'
|
|
6
|
+
require 'em-xmpp/resolver'
|
|
7
|
+
|
|
8
|
+
module EM::Xmpp
|
|
9
|
+
module Connector
|
|
10
|
+
include Namespaces
|
|
11
|
+
|
|
12
|
+
#XML SAX document which delegates its method to a recipient object
|
|
13
|
+
class ForwardingDocument < Nokogiri::XML::SAX::Document
|
|
14
|
+
attr_accessor :recipient
|
|
15
|
+
%w{xmldecl start_document end_document start_element_namespace end_element characters
|
|
16
|
+
comment warning error cdata_block}.each do |meth|
|
|
17
|
+
meth2 = "xml_#{meth}"
|
|
18
|
+
define_method(meth) do |*args|
|
|
19
|
+
recipient.send(meth2, *args) if recipient
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.included(obj)
|
|
25
|
+
obj.extend ClassMethods
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module ClassMethods
|
|
29
|
+
def start(jid, pass=nil, mod=nil, cfg={}, server=nil, port=5222, &blk)
|
|
30
|
+
jid = JID.parse jid
|
|
31
|
+
if server.nil?
|
|
32
|
+
record = Resolver.resolve jid.domain
|
|
33
|
+
if record
|
|
34
|
+
server = record.target.to_s
|
|
35
|
+
port = record.port
|
|
36
|
+
else
|
|
37
|
+
server = jid.domain
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
EM.connect(server, port, self, jid, pass, mod, cfg, &blk)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
extend ClassMethods
|
|
46
|
+
|
|
47
|
+
def send_raw(data)
|
|
48
|
+
puts ">> out\n#{data}\n" if $DEBUG
|
|
49
|
+
send_data data
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def send_xml(&blk)
|
|
53
|
+
data = build_xml(&blk)
|
|
54
|
+
send_raw data
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def restart_xml_stream
|
|
58
|
+
@xml_parser.document.recipient = nil
|
|
59
|
+
post_init
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def post_init
|
|
63
|
+
doc = ForwardingDocument.new
|
|
64
|
+
doc.recipient = self
|
|
65
|
+
@xml_parser = Nokogiri::XML::SAX::PushParser.new doc
|
|
66
|
+
@stack = []
|
|
67
|
+
@stanza = nil
|
|
68
|
+
@streamdoc = nil
|
|
69
|
+
|
|
70
|
+
open_xml_stream
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def receive_data(dat)
|
|
74
|
+
puts "<< in\n#{dat}\n" if $DEBUG
|
|
75
|
+
@xml_parser << dat
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def unbind
|
|
79
|
+
puts "**** unbound ****" if $DEBUG
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_xml(&blk)
|
|
83
|
+
n = Nokogiri::XML::Builder.new(&blk)
|
|
84
|
+
n.doc.root.to_xml
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def open_xml_stream_tag
|
|
90
|
+
domain = @jid.domain
|
|
91
|
+
version = '1.0'
|
|
92
|
+
lang = 'en'
|
|
93
|
+
start_stream = <<-STREAM
|
|
94
|
+
<stream:stream
|
|
95
|
+
to='#{domain}'
|
|
96
|
+
version='#{version}'
|
|
97
|
+
xml:lang='#{lang}'
|
|
98
|
+
xmlns='#{Client}'
|
|
99
|
+
xmlns:stream='#{Stream}'
|
|
100
|
+
>
|
|
101
|
+
STREAM
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def close_xml_stream_tag
|
|
105
|
+
'</stream:stream>'
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def open_xml_stream
|
|
109
|
+
send_raw open_xml_stream_tag
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def close_xml_stream
|
|
113
|
+
send_raw close_xml_stream_tag
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
### XML world
|
|
117
|
+
|
|
118
|
+
def xml_xmldecl(version,encoding,standalone)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def xml_start_document
|
|
122
|
+
#XXX set namespaces and stream prefix
|
|
123
|
+
# namespace may depend on the type of connection ('jabber:client' or
|
|
124
|
+
# 'jabber:server')
|
|
125
|
+
# currently we do not set any stream's namespace, hence when builidng stanza,
|
|
126
|
+
# we must explicitely avoid writing the namespace of iq/presence/message XML nodes
|
|
127
|
+
@streamdoc = Nokogiri::XML::Document.new
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def xml_end_document
|
|
131
|
+
@streamdoc = @stanza = @stack = @xml_parser = nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def xml_start_element_namespace(name, attrs=[],prefix=nil,uri=nil,ns=[])
|
|
135
|
+
node = Nokogiri::XML::Node.new(name, @streamdoc)
|
|
136
|
+
attrs.each do |attr|
|
|
137
|
+
#attr is a Struct with members localname/prefix/uri/value
|
|
138
|
+
node[attr.localname] = attr.value
|
|
139
|
+
end
|
|
140
|
+
#XXX - if prefix is there maybe we do not want to set uri as default
|
|
141
|
+
node.default_namespace = uri if uri
|
|
142
|
+
|
|
143
|
+
ns.each do |pfx,href|
|
|
144
|
+
node.add_namespace_definition pfx, href
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
puts "starting: #{name}, stack:#{@stack.size}" if $DEBUG
|
|
148
|
+
case @stack.size
|
|
149
|
+
when 0 #the streaming tag starts
|
|
150
|
+
stream_support(node)
|
|
151
|
+
when 1 #a stanza starts
|
|
152
|
+
set_current_stanza!(node)
|
|
153
|
+
stanza_start node
|
|
154
|
+
else
|
|
155
|
+
@stack.last.add_child node
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
@stack << node
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def xml_end_element(name)
|
|
162
|
+
node = @stack.pop
|
|
163
|
+
puts "ending: #{name}, stack:#{@stack.size}" if $DEBUG
|
|
164
|
+
|
|
165
|
+
case @stack.size
|
|
166
|
+
when 0 #i.e., the stream support ends
|
|
167
|
+
xml_stream_closing
|
|
168
|
+
when 1 #i.e., we've finished a stanza
|
|
169
|
+
raise RuntimeError, "should end on a stanza" unless node == @stanza
|
|
170
|
+
stanza_end node
|
|
171
|
+
else
|
|
172
|
+
#the stanza keeps growing
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def xml_characters(txt)
|
|
177
|
+
@stack.last << Nokogiri::XML::Text.new(txt, @streamdoc)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def xml_error(err)
|
|
181
|
+
#raise RuntimeError, err
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def xml_stream_closing
|
|
185
|
+
close_xml_stream
|
|
186
|
+
close_connection
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def xml_comment(comm)
|
|
190
|
+
raise NotImplementedError
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def xml_warning(warn)
|
|
194
|
+
raise NotImplementedError
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def xml_cdata_block(data)
|
|
198
|
+
raise NotImplementedError
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
### XMPP World
|
|
202
|
+
|
|
203
|
+
def stream_support(node)
|
|
204
|
+
@stanza = Nokogiri::XML::Node.new('dummy', @streamdoc)
|
|
205
|
+
node << @stanza
|
|
206
|
+
|
|
207
|
+
@streamdoc.root = node
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def set_current_stanza!(node)
|
|
211
|
+
@stanza.remove
|
|
212
|
+
|
|
213
|
+
@stanza = node
|
|
214
|
+
@streamdoc.root << @stanza
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def stanza_start(node)
|
|
218
|
+
raise NotImplementedError
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def stanza_end(node)
|
|
222
|
+
raise NotImplementedError
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
public
|
|
226
|
+
|
|
227
|
+
### TLS World
|
|
228
|
+
|
|
229
|
+
def ask_for_tls
|
|
230
|
+
send_xml do |x|
|
|
231
|
+
x.starttls(:xmlns => TLS)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def start_using_tls_and_reset_stream
|
|
236
|
+
start_tls(:verify_peer => false)
|
|
237
|
+
restart_xml_stream
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def ssl_verify_peer(pem)
|
|
241
|
+
raise NotImplementedError
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
|
|
2
|
+
require 'em-xmpp/jid'
|
|
3
|
+
require 'em-xmpp/namespaces'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module EM::Xmpp
|
|
7
|
+
class Context
|
|
8
|
+
attr_reader :connection, :stanza, :env
|
|
9
|
+
|
|
10
|
+
def []key
|
|
11
|
+
env[key]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def []=key,val
|
|
15
|
+
env[key]= val
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def default_env
|
|
19
|
+
{'modules' => [], 'done' => false}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def done!
|
|
23
|
+
env['ctx.done'] = true
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def done?
|
|
28
|
+
env['ctx.done']
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete_xpath_handler!
|
|
32
|
+
env.delete 'xpath.handler'
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reuse_handler?
|
|
37
|
+
env['xpath.handler']
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(conn, stanza, env={})
|
|
41
|
+
@connection = conn
|
|
42
|
+
@stanza = stanza
|
|
43
|
+
@env = default_env.merge env
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def xpath(path, args={})
|
|
47
|
+
stanza.xpath(path, args) || []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def xpath?(path, args={})
|
|
51
|
+
xpath(path, args).any?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def read_attr(node, name, sym=:to_s, &blk)
|
|
55
|
+
val = node[name]
|
|
56
|
+
if val
|
|
57
|
+
if blk
|
|
58
|
+
blk.call val
|
|
59
|
+
else
|
|
60
|
+
val.send sym
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def with(modname)
|
|
66
|
+
mod = if modname.is_a?(Module)
|
|
67
|
+
modname
|
|
68
|
+
else
|
|
69
|
+
self.class.const_get(modname.to_s.capitalize)
|
|
70
|
+
end
|
|
71
|
+
env['modules'] << mod
|
|
72
|
+
obj = self
|
|
73
|
+
obj.extend mod
|
|
74
|
+
obj
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
module Stanza
|
|
78
|
+
include Namespaces
|
|
79
|
+
%w{type id lang}.each do |w|
|
|
80
|
+
define_method w do
|
|
81
|
+
read_attr stanza, w
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
def to
|
|
85
|
+
read_attr(stanza, 'to'){|j| JID.parse(j)}
|
|
86
|
+
end
|
|
87
|
+
def from
|
|
88
|
+
read_attr(stanza, 'from'){|j| JID.parse(j)}
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
module Presence
|
|
93
|
+
include Stanza
|
|
94
|
+
def reply_default_params
|
|
95
|
+
jid = @connection.jid.full
|
|
96
|
+
{'from' => jid, 'to' => from, 'id' => id}
|
|
97
|
+
end
|
|
98
|
+
def reply(args={},&blk)
|
|
99
|
+
args = reply_default_params.merge args
|
|
100
|
+
@connection.presence_stanza(args,&blk)
|
|
101
|
+
end
|
|
102
|
+
def priority_node
|
|
103
|
+
xpath('//xmlns:priority',{'xmlns' => Client}).first
|
|
104
|
+
end
|
|
105
|
+
def status_node
|
|
106
|
+
xpath('//xmlns:status',{'xmlns' => Client}).first
|
|
107
|
+
end
|
|
108
|
+
def show_node
|
|
109
|
+
xpath('//xmlns:show',{'xmlns' => Client}).first
|
|
110
|
+
end
|
|
111
|
+
def priority
|
|
112
|
+
node = priority_node
|
|
113
|
+
node.content if node
|
|
114
|
+
end
|
|
115
|
+
def status
|
|
116
|
+
node = status_node
|
|
117
|
+
node.content if node
|
|
118
|
+
end
|
|
119
|
+
def show
|
|
120
|
+
node = show_node
|
|
121
|
+
node.content if node
|
|
122
|
+
end
|
|
123
|
+
def subscription_request?
|
|
124
|
+
type == 'subscribe'
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
module Message
|
|
129
|
+
include Stanza
|
|
130
|
+
def subject_node
|
|
131
|
+
xpath('//xmlns:subject',{'xmlns' => Client}).first
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def subject
|
|
135
|
+
node = subject_node
|
|
136
|
+
node.text if node
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def body_node
|
|
140
|
+
xpath('//xmlns:body',{'xmlns' => Client}).first
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def body
|
|
144
|
+
node = body_node
|
|
145
|
+
node.text if node
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def reply_default_params
|
|
149
|
+
h = {'to' => from, 'type' => type}
|
|
150
|
+
h['id'] = id.succ if id
|
|
151
|
+
h
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def reply(args={},&blk)
|
|
155
|
+
args = reply_default_params.merge args
|
|
156
|
+
@connection.message_stanza(args,&blk)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
module Iq
|
|
161
|
+
include Stanza
|
|
162
|
+
def reply_default_params
|
|
163
|
+
jid = @connection.jid.full
|
|
164
|
+
{'from' => jid, 'to' => from, 'type' => 'result', 'id' => id}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def reply(args={},&blk)
|
|
168
|
+
args = reply_default_params.merge args
|
|
169
|
+
@connection.iq_stanza(args,&blk)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
module Delay
|
|
174
|
+
#does not handle legacy delay
|
|
175
|
+
def delay_node
|
|
176
|
+
xpath('//xmlns:delay',{'xmlns' => EM::Xmpp::Namespaces::Delay}).first
|
|
177
|
+
end
|
|
178
|
+
def stamp
|
|
179
|
+
n = delay_node
|
|
180
|
+
Time.parse read_attr(n, 'stamp') if n
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
module Discoveries
|
|
185
|
+
include Iq
|
|
186
|
+
%w{node}.each do |word|
|
|
187
|
+
define_method word do
|
|
188
|
+
n = query_node
|
|
189
|
+
read_attr(n, word) if n
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
module Discoinfos
|
|
195
|
+
include Discoveries
|
|
196
|
+
def query_node
|
|
197
|
+
xpath('//xmlns:query',{'xmlns' => DiscoverInfos}).first
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
module Discoitems
|
|
202
|
+
include Discoveries
|
|
203
|
+
def query_node
|
|
204
|
+
xpath('//xmlns:query',{'xmlns' => DiscoverItems}).first
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
module Command
|
|
209
|
+
def command_node
|
|
210
|
+
xpath('//xmlns:command',{'xmlns' => Commands}).first
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
%w{node sessionid action}.each do |word|
|
|
214
|
+
define_method word do
|
|
215
|
+
n = command_node
|
|
216
|
+
read_attr(n, word) if n
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def previous?
|
|
221
|
+
action == 'prev'
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
module Dataforms
|
|
226
|
+
def x_node
|
|
227
|
+
xpath('//xmlns:x',{'xmlns' => DataForms}).first
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def x_type
|
|
231
|
+
n = x_node
|
|
232
|
+
read_attr(n, 'type') if n
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
module Capabilities
|
|
237
|
+
def c_node
|
|
238
|
+
xpath('//xmlns:c',{'xmlns' => EM::Xmpp::Namespaces::Capabilities}).first
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
%w{node ver ext}.each do |word|
|
|
242
|
+
define_method word do
|
|
243
|
+
n = c_node
|
|
244
|
+
read_attr(n, word) if n
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
|
|
2
|
+
require 'em-xmpp/namespaces'
|
|
3
|
+
require 'em-xmpp/context'
|
|
4
|
+
require 'base64'
|
|
5
|
+
require 'sasl/base'
|
|
6
|
+
require 'sasl'
|
|
7
|
+
|
|
8
|
+
module EM::Xmpp
|
|
9
|
+
class Handler
|
|
10
|
+
include Namespaces
|
|
11
|
+
|
|
12
|
+
Xpath = Struct.new(:path, :args, :blk) do
|
|
13
|
+
def match?(xml)
|
|
14
|
+
path == :anything or xml.xpath(path, args).any?
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(conn)
|
|
19
|
+
@connection = conn
|
|
20
|
+
@xpaths = []
|
|
21
|
+
@exception_xpaths = []
|
|
22
|
+
|
|
23
|
+
stack_decorators
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def on_presence(&blk)
|
|
27
|
+
on('//xmlns:presence', 'xmlns' => EM::Xmpp::Namespaces::Client, &blk)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_message(&blk)
|
|
31
|
+
on('//xmlns:message', 'xmlns' => EM::Xmpp::Namespaces::Client, &blk)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def on_iq(&blk)
|
|
35
|
+
on('//xmlns:iq', 'xmlns' => EM::Xmpp::Namespaces::Client, &blk)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def stack_decorators
|
|
39
|
+
on_presence { |ctx| ctx.with(:presence) }
|
|
40
|
+
on_message { |ctx| ctx.with(:message) }
|
|
41
|
+
on_iq { |ctx| ctx.with(:iq) }
|
|
42
|
+
on('//xmlns:delay', 'xmlns' => Delay) do |ctx|
|
|
43
|
+
ctx.with(:delay)
|
|
44
|
+
end
|
|
45
|
+
on('//xmlns:query', 'xmlns' => DiscoverInfos) do |ctx|
|
|
46
|
+
ctx.with(:discoinfos)
|
|
47
|
+
end
|
|
48
|
+
on('//xmlns:query', 'xmlns' => DiscoverItems) do |ctx|
|
|
49
|
+
ctx.with(:discoitems)
|
|
50
|
+
end
|
|
51
|
+
on('//xmlns:command', 'xmlns' => Commands) do |ctx|
|
|
52
|
+
ctx.with(:command)
|
|
53
|
+
end
|
|
54
|
+
on('//xmlns:x', 'xmlns' => DataForms) do |ctx|
|
|
55
|
+
ctx.with(:dataforms)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def on(path, args={}, &blk)
|
|
60
|
+
@xpaths << Xpath.new(path, args, blk)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def on_exception(path, args={}, &blk)
|
|
64
|
+
@exception_xpaths << Xpath.new(path, args, blk)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# wraps the stanza in a context and calls handle_context
|
|
68
|
+
def handle(stanza)
|
|
69
|
+
handle_context Context.new(@connection, stanza)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# runs all xpath_handlers against the stanza context
|
|
73
|
+
# catches all exception (in which case, the context gets passed to all
|
|
74
|
+
# exception_xpaths)
|
|
75
|
+
#
|
|
76
|
+
# an xpath handler can:
|
|
77
|
+
# - throw :halt to shortcircuit everything
|
|
78
|
+
# - set the context to "done!" to avoid invoking handlers
|
|
79
|
+
# - delete_xpath_handler from the history, this is useful in one-time
|
|
80
|
+
# handlers such as request/responses
|
|
81
|
+
def handle_context(ctx)
|
|
82
|
+
catch :halt do
|
|
83
|
+
@xpaths = run_xpath_handlers ctx, @xpaths
|
|
84
|
+
end
|
|
85
|
+
rescue => err
|
|
86
|
+
ctx['error'] = err
|
|
87
|
+
@exception_xpaths = run_xpath_handlers ctx, @exception_xpaths
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# runs all handlers and returns a list of handlers for the next stanza
|
|
91
|
+
def run_xpath_handlers(ctx, handlers)
|
|
92
|
+
handlers.map do |x|
|
|
93
|
+
if (not ctx.done?) and (x.match?(ctx.stanza))
|
|
94
|
+
ctx['xpath.handler'] = x
|
|
95
|
+
ctx = x.blk.call(ctx)
|
|
96
|
+
raise RuntimeError, "xpath handlers should return a Context" unless ctx.is_a?(Context)
|
|
97
|
+
|
|
98
|
+
x if ctx.reuse_handler?
|
|
99
|
+
else
|
|
100
|
+
x
|
|
101
|
+
end
|
|
102
|
+
end.compact
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class XmppSASL < ::SASL::Preferences
|
|
107
|
+
attr_accessor :handler
|
|
108
|
+
def initialize(handler)
|
|
109
|
+
@handler = handler
|
|
110
|
+
end
|
|
111
|
+
def realm
|
|
112
|
+
handler.jid.domain
|
|
113
|
+
end
|
|
114
|
+
def digest_uri
|
|
115
|
+
'xmpp/' + handler.jid.domain
|
|
116
|
+
end
|
|
117
|
+
def username
|
|
118
|
+
handler.jid.node
|
|
119
|
+
end
|
|
120
|
+
def has_password?
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
def password
|
|
124
|
+
ret = handler.pass
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class Routine < Handler
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class StreamNegotiation < Handler
|
|
132
|
+
attr_reader :sasl
|
|
133
|
+
|
|
134
|
+
def initialize(conn)
|
|
135
|
+
super conn
|
|
136
|
+
@sasl = nil
|
|
137
|
+
setup_handlers
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def c
|
|
141
|
+
@connection
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def jid
|
|
145
|
+
@connection.jid
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def pass
|
|
149
|
+
@connection.pass
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def setup_handlers
|
|
153
|
+
on('//xmlns:starttls', {'xmlns' => TLS}) do |ctx|
|
|
154
|
+
@connection.ask_for_tls
|
|
155
|
+
ctx.delete_xpath_handler!.done!
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
on('//xmlns:proceed', {'xmlns' => TLS }) do |ctx|
|
|
159
|
+
@connection.start_using_tls_and_reset_stream
|
|
160
|
+
ctx.delete_xpath_handler!.done!
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
on('//xmlns:mechanisms', {'xmlns' => SASL}) do |ctx|
|
|
164
|
+
search = ctx.xpath('//xmlns:mechanisms', {'xmlns' => SASL})
|
|
165
|
+
if search.first
|
|
166
|
+
mechanisms = search.first.children.map(&:content)
|
|
167
|
+
start_sasl mechanisms
|
|
168
|
+
ctx.delete_xpath_handler!.done!
|
|
169
|
+
else
|
|
170
|
+
raise RuntimeError, "how come there is no mechanism node?"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
on('//xmlns:challenge', {'xmlns' => SASL}) do |ctx|
|
|
175
|
+
sasl_step ctx.stanza
|
|
176
|
+
ctx.done!
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
on('//xmlns:success', {'xmlns' => SASL}) do |ctx|
|
|
180
|
+
@connection.restart_xml_stream
|
|
181
|
+
ctx.delete_xpath_handler!.done!
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
on('//xmlns:bind', {'xmlns' => Bind}) do |ctx|
|
|
185
|
+
bind_to_resource
|
|
186
|
+
ctx.delete_xpath_handler!.done!
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
on('//xmlns:bind', {'xmlns' => Bind}) do |ctx|
|
|
190
|
+
jid = extract_jid ctx.stanza
|
|
191
|
+
|
|
192
|
+
if jid
|
|
193
|
+
@connection.jid_received jid
|
|
194
|
+
start_session
|
|
195
|
+
else
|
|
196
|
+
raise RuntimeError, "no jid despite binding"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
ctx.delete_xpath_handler!.done!
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
on('//xmlns:session', {'xmlns' => Session}) do |ctx|
|
|
203
|
+
@connection.negotiation_finished
|
|
204
|
+
ctx.delete_xpath_handler!.done!
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
on('//xmlns:failure', {'xmlns' => SASL}) do |ctx|
|
|
208
|
+
@connection.negotiation_failed(ctx.stanza)
|
|
209
|
+
ctx.done!
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
on(:anything) do |ctx|
|
|
213
|
+
@connection.unhandled ctx.stanza
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def extract_jid(stanza)
|
|
218
|
+
jid = stanza.xpath('//bind:jid', {'bind' => Bind})
|
|
219
|
+
jid.text if jid.any?
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def bind_to_resource(wanted_res=nil)
|
|
223
|
+
c.send_raw(c.iq_stanza('type' => 'set') do |x|
|
|
224
|
+
x.bind('xmlns' => Bind) do |y|
|
|
225
|
+
y.resource(wanted_res) if wanted_res
|
|
226
|
+
end
|
|
227
|
+
end)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def start_session
|
|
231
|
+
c.send_raw(c.iq_stanza('type' => 'set', 'to' => jid.domain) do |x|
|
|
232
|
+
x.session('xmlns' => Session)
|
|
233
|
+
end)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def start_sasl(methods)
|
|
237
|
+
@sasl = ::SASL.new(methods, XmppSASL.new(self))
|
|
238
|
+
msg,val = sasl.start
|
|
239
|
+
mech = sasl.mechanism
|
|
240
|
+
reply_sasl(msg,val,mech)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def sasl_step(stanza)
|
|
244
|
+
msg = stanza.name
|
|
245
|
+
inStr = Base64.strict_decode64(stanza.text)
|
|
246
|
+
meth,str = sasl.receive msg, inStr
|
|
247
|
+
b64 = str ? Base64.strict_encode64(str) : ''
|
|
248
|
+
reply_sasl(meth, b64)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def reply_sasl(msg, val=nil, mech=nil)
|
|
252
|
+
c.send_xml do |x|
|
|
253
|
+
if val
|
|
254
|
+
x.send(msg, val, {'xmlns' => SASL, 'mechanism' => mech})
|
|
255
|
+
else
|
|
256
|
+
x.send(msg, {'xmlns' => SASL, 'mechanism' => mech})
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
end
|
|
262
|
+
end
|
data/lib/em-xmpp/jid.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
module EM::Xmpp
|
|
4
|
+
JID = Struct.new(:node, :domain, :resource) do
|
|
5
|
+
def self.parse(str)
|
|
6
|
+
s1,s2 = str.split('@',2)
|
|
7
|
+
if s2.nil?
|
|
8
|
+
self.new(nil, s1, nil)
|
|
9
|
+
else
|
|
10
|
+
s2,s3 = s2.split('/',2)
|
|
11
|
+
self.new(s1,s2,s3)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def bare
|
|
16
|
+
[node,domain].map(&:to_s).join('@')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def full
|
|
20
|
+
[bare,resource].map(&:to_s).join('/')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_s
|
|
24
|
+
full
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
module EM::Xmpp
|
|
3
|
+
# A handy module with the XMPP namespaces name.
|
|
4
|
+
module Namespaces
|
|
5
|
+
Client = 'jabber:client'
|
|
6
|
+
#In-band registration
|
|
7
|
+
Registration = 'http://jabber.org/features/iq-register'
|
|
8
|
+
#XMPP stream-level stanza
|
|
9
|
+
Stream = 'http://etherx.jabber.org/streams'
|
|
10
|
+
#TLS negotiation
|
|
11
|
+
TLS = 'urn:ietf:params:xml:ns:xmpp-tls'
|
|
12
|
+
#SASL authentication
|
|
13
|
+
SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
|
14
|
+
#XMPP resource binding
|
|
15
|
+
Bind = 'urn:ietf:params:xml:ns:xmpp-bind'
|
|
16
|
+
#XMPP session creation
|
|
17
|
+
Session = 'urn:ietf:params:xml:ns:xmpp-session'
|
|
18
|
+
#XMPP capabilities discovery
|
|
19
|
+
Capabilities = "http://jabber.org/protocol/caps"
|
|
20
|
+
#XMPP item discovery
|
|
21
|
+
DiscoverItems = "http://jabber.org/protocol/disco#items"
|
|
22
|
+
#XMPP info discovery
|
|
23
|
+
DiscoverInfos = "http://jabber.org/protocol/disco#infos"
|
|
24
|
+
#XMPP commands
|
|
25
|
+
Commands = "http://jabber.org/protocol/commands"
|
|
26
|
+
#XMPP delayed delivery
|
|
27
|
+
Delay = "urn:xmpp:delay"
|
|
28
|
+
#Jabber Data forms
|
|
29
|
+
DataForms = 'jabber:x:data'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
require 'resolv'
|
|
3
|
+
module EM::Xmpp
|
|
4
|
+
module Resolver
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def resolve(domain)
|
|
8
|
+
resolve_all(domain).first
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def resolve_all(domain)
|
|
12
|
+
srv = []
|
|
13
|
+
Resolv::DNS.open do |dns|
|
|
14
|
+
record = "_xmpp-client._tcp.#{domain}"
|
|
15
|
+
srv = dns.getresources(record, Resolv::DNS::Resource::IN::SRV)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
srv.sort do |a,b|
|
|
19
|
+
(a.priority != b.priority) ? (a.priority <=> b.priority) : (b.weight <=> a.weight)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/em-xmpp.rb
ADDED
data/samples/echo.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
$LOAD_PATH.unshift './lib'
|
|
3
|
+
require 'em-xmpp'
|
|
4
|
+
require 'em-xmpp/namespaces'
|
|
5
|
+
require 'em-xmpp/nodes'
|
|
6
|
+
|
|
7
|
+
if ARGV.empty?
|
|
8
|
+
puts "usage: #{__FILE__} <jid> [<pass>]"
|
|
9
|
+
exit 0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
jid = ARGV.first
|
|
13
|
+
pass = ARGV[1]
|
|
14
|
+
|
|
15
|
+
include EM::Xmpp::Namespaces
|
|
16
|
+
include EM::Xmpp::Nodes
|
|
17
|
+
|
|
18
|
+
module MyClient
|
|
19
|
+
def ready
|
|
20
|
+
puts "***** #{@jid} ready for #{self}"
|
|
21
|
+
|
|
22
|
+
on_presence do |s|
|
|
23
|
+
p "*presence> #{s.from} #{s.show} (#{s.status})"
|
|
24
|
+
send_raw(s.reply('type'=>'subscribed')) if s.subscription_request?
|
|
25
|
+
s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
on_message do |s|
|
|
29
|
+
p "*message> #{s.from}\n#{s.body}\n"
|
|
30
|
+
send_raw(s.reply do |x|
|
|
31
|
+
x.body "you sent:#{s.body}"
|
|
32
|
+
end)
|
|
33
|
+
s
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
EM.run do
|
|
39
|
+
EM::Xmpp::Connection.start(jid, pass, MyClient)
|
|
40
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: em-xmpp
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- crapooze
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-04-27 00:00:00.000000000Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: eventmachine
|
|
16
|
+
requirement: &2153101140 !ruby/object:Gem::Requirement
|
|
17
|
+
none: false
|
|
18
|
+
requirements:
|
|
19
|
+
- - ! '>='
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: *2153101140
|
|
25
|
+
- !ruby/object:Gem::Dependency
|
|
26
|
+
name: nokogiri
|
|
27
|
+
requirement: &2153100720 !ruby/object:Gem::Requirement
|
|
28
|
+
none: false
|
|
29
|
+
requirements:
|
|
30
|
+
- - ! '>='
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: *2153100720
|
|
36
|
+
- !ruby/object:Gem::Dependency
|
|
37
|
+
name: ruby-sasl
|
|
38
|
+
requirement: &2153100300 !ruby/object:Gem::Requirement
|
|
39
|
+
none: false
|
|
40
|
+
requirements:
|
|
41
|
+
- - ! '>='
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
type: :runtime
|
|
45
|
+
prerelease: false
|
|
46
|
+
version_requirements: *2153100300
|
|
47
|
+
description: XMPP client for event machine
|
|
48
|
+
email:
|
|
49
|
+
- crapooze@gmail.com
|
|
50
|
+
executables: []
|
|
51
|
+
extensions: []
|
|
52
|
+
extra_rdoc_files: []
|
|
53
|
+
files:
|
|
54
|
+
- .gitignore
|
|
55
|
+
- Gemfile
|
|
56
|
+
- LICENSE
|
|
57
|
+
- README.md
|
|
58
|
+
- Rakefile
|
|
59
|
+
- em-xmpp.gemspec
|
|
60
|
+
- lib/em-xmpp.rb
|
|
61
|
+
- lib/em-xmpp/cert_store.rb
|
|
62
|
+
- lib/em-xmpp/connection.rb
|
|
63
|
+
- lib/em-xmpp/connector.rb
|
|
64
|
+
- lib/em-xmpp/context.rb
|
|
65
|
+
- lib/em-xmpp/handler.rb
|
|
66
|
+
- lib/em-xmpp/jid.rb
|
|
67
|
+
- lib/em-xmpp/namespaces.rb
|
|
68
|
+
- lib/em-xmpp/nodes.rb
|
|
69
|
+
- lib/em-xmpp/resolver.rb
|
|
70
|
+
- lib/em-xmpp/version.rb
|
|
71
|
+
- samples/echo.rb
|
|
72
|
+
homepage: https://github.com/crapooze/em-xmpp
|
|
73
|
+
licenses: []
|
|
74
|
+
post_install_message:
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
none: false
|
|
80
|
+
requirements:
|
|
81
|
+
- - ! '>='
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
|
+
none: false
|
|
86
|
+
requirements:
|
|
87
|
+
- - ! '>='
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
requirements: []
|
|
91
|
+
rubyforge_project:
|
|
92
|
+
rubygems_version: 1.8.6
|
|
93
|
+
signing_key:
|
|
94
|
+
specification_version: 3
|
|
95
|
+
summary: Easy to write and to extend XMPP client
|
|
96
|
+
test_files: []
|