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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in em-xmpp.gemspec
4
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
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
@@ -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,8 @@
1
+
2
+ module EM::Xmpp
3
+ # A handy module with the XMPP discovery and pubsub names.
4
+ # see http://xmpp.org/registrar/nodes.html
5
+ module Nodes
6
+ Commands = 'http://jabber.org/protocol/commands'
7
+ end
8
+ 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
@@ -0,0 +1,5 @@
1
+ module Em
2
+ module Xmpp
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
data/lib/em-xmpp.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'eventmachine'
2
+ require "em-xmpp/version"
3
+ require "em-xmpp/connection"
4
+
5
+ module EM
6
+ module Xmpp
7
+ # Your code goes here...
8
+ end
9
+ end
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: []