em-xmpp 0.0.1

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