radamanthus-skates 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ module Skates
2
+ module Base
3
+ ##
4
+ # Class used to Parse a Stanza on the XMPP stream.
5
+ # You should have a Stanza subsclass for each of your controller actions, as they allow you to define which stanzas and which information is passed to yoru controllers.
6
+ # These classes extend the Nokogiri::XML::Node
7
+ # You can define your own accessors to access the content uou need, using XPath.
8
+
9
+ # if your stanza is a message stanza, you can match the following for example:
10
+ # element :message, :value => :to, :as => :to
11
+ # element :message, :value => :from, :as => :from
12
+ # element :message, :value => :id, :as => :stanza_id
13
+ # element :message, :value => :type, :as => :stanza_type
14
+ # element :message, :value => :"xml:lang", :as => :lang
15
+ #
16
+ class Stanza
17
+
18
+ def initialize(node)
19
+ @node = node
20
+ end
21
+
22
+ def from
23
+ @node.at_xpath(".")["from"]
24
+ end
25
+
26
+ def to
27
+ @node.at_xpath(".")["to"]
28
+ end
29
+
30
+ def id
31
+ @node.at_xpath(".")["id"]
32
+ end
33
+
34
+ def type
35
+ @node.at_xpath(".")["type"]
36
+ end
37
+
38
+ def name
39
+ @node.at_xpath(".").name
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,59 @@
1
+ module Skates
2
+ module Base
3
+
4
+ class ViewFileNotFound < Errno::ENOENT; end
5
+
6
+ ##
7
+ # Your application's views (stanzas) should be descendant of this class.
8
+ class View
9
+ attr_reader :view_template
10
+
11
+ ##
12
+ # Used to 'include' another view inside an existing view.
13
+ # The caller needs to pass the context in which the partial will be rendered
14
+ # Render must be called with :partial as well (other options will be supported later). The partial vale should be a relative path
15
+ # to another file view, from the calling view.
16
+ # You can also use :locals => {:name => value} to use defined locals in your embedded views.
17
+ def render(xml, options = {})
18
+ # First, we need to identify the partial file path, based on the @view_template path.
19
+ partial_path = (@view_template.split("/")[0..-2] + options[:partial].split("/")).join("/").gsub(".xml.builder", "") + ".xml.builder"
20
+ partial_path = Pathname.new(partial_path).cleanpath.to_s
21
+ raise ViewFileNotFound, "No such file #{partial_path}" unless Skates.views[partial_path]
22
+ saved_locals = @locals
23
+ @locals = options[:locals]
24
+ eval(Skates.views[partial_path], binding, partial_path, 1)
25
+ @locals = saved_locals # Re-assign the previous locals to be 'clean'
26
+ end
27
+
28
+ ##
29
+ # Instantiate a new view with the various varibales passed in assigns and the path of the template to render.
30
+ def initialize(path = "", assigns = {})
31
+ @view_template = path
32
+ @locals = {}
33
+ assigns.each do |key, value|
34
+ instance_variable_set(:"@#{key}", value)
35
+ end
36
+ end
37
+
38
+ ##
39
+ # "Loads" the view file, and uses the Nokogiri Builder to build the XML stanzas that will be sent.
40
+ def evaluate
41
+ return if @view_template == ""
42
+ raise ViewFileNotFound, "No such file #{@view_template}" unless Skates.views[@view_template]
43
+ builder = Nokogiri::XML::Builder.new
44
+ builder.stream do |xml|
45
+ eval(Skates.views[@view_template], binding, @view_template, 1)
46
+ end
47
+ builder.doc.root.children # we output the document built
48
+ end
49
+
50
+ ##
51
+ # Used to macth locals variables
52
+ def method_missing(sym, *args, &block)
53
+ raise NameError, "undefined local variable or method `#{sym}' for #{self}" unless @locals[sym]
54
+ @locals[sym]
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,234 @@
1
+ module Skates
2
+
3
+ ##
4
+ # ClientConnection is in charge of the XMPP connection for a Regular XMPP Client.
5
+ # So far, SASL Plain authenticationonly is supported
6
+ # Upon stanza reception, and depending on the status (connected... etc), this component will handle or forward the stanzas.
7
+ class ClientConnection < XmppConnection
8
+
9
+ attr_reader :binding_iq_id, :session_iq_id
10
+
11
+ ##
12
+ # Creates a new ClientConnection and waits for data in the stream
13
+ def initialize(params)
14
+ super(params)
15
+ @state = :wait_for_stream
16
+ end
17
+
18
+ ##
19
+ # Connects the ClientConnection based on SRV records for the jid's domain, if no host has been provided.
20
+ # It will not resolve if params["host"] is an IP.
21
+ # And it will always use
22
+ def self.connect(params, handler = nil)
23
+ params["host"] ||= "_xmpp-client._tcp." + params["jid"].split("/").first.split("@").last
24
+ super(params, handler)
25
+ end
26
+
27
+ ##
28
+ # Resolution for clients, based on SRV records
29
+ def self.resolve(host, &block)
30
+ Resolv::DNS.open { |dns|
31
+ # If ruby version is too old and SRV is unknown, this will raise a NameError
32
+ # which is caught below
33
+ begin
34
+ found = false
35
+
36
+ Skates.logger.debug {
37
+ "RESOLVING: #{host} (SRV)"
38
+ }
39
+ srv = dns.getresources(host, Resolv::DNS::Resource::IN::SRV)
40
+ # Sort SRV records: lowest priority first, highest weight first
41
+ srv.sort! { |a,b| (a.priority != b.priority) ? (a.priority <=> b.priority) : (b.weight <=> a.weight) }
42
+ Skates.logger.debug {
43
+ "Found #{srv.count} SRV records for : #{host} : #{srv.inspect}"
44
+ }
45
+ # And now, for each record, let's try to connect.
46
+ srv.each { |record|
47
+ Skates.logger.debug {
48
+ "Trying connection with : #{record.target.to_s}:#{record.port}"
49
+ }
50
+ ip = record.target.to_s
51
+ port = Integer(record.port)
52
+ if block.call({"host" => ip, "port" => port})
53
+ found = true
54
+ break
55
+ end
56
+ }
57
+ if !found
58
+ # We failover to solving A record with default port.
59
+ Skates.logger.debug {
60
+ "RESOLVING: #{host} (A record)"
61
+ }
62
+ records = dns.getresources(host, Resolv::DNS::Resource::IN::A)
63
+ records.each do |record|
64
+ ip = record.address.to_s
65
+ if block.call({"host" => ip, "port" => Integer(Skates.config["port"]) || 5222})
66
+ found = true
67
+ break
68
+ end
69
+ end
70
+ block.call(false) unless found# bleh, we couldn't resolve to host that accept connections. Too bad.
71
+ end
72
+ rescue NameError
73
+ Skates.logger.debug {
74
+ "Resolv::DNS does not support SRV records. Please upgrade to ruby-1.8.3 or later! \n#{$!} : #{$!.backtrace.join("\n")}"
75
+ }
76
+ end
77
+ }
78
+ end
79
+
80
+ ##
81
+ # Builds the stream stanza for this client
82
+ def stream_stanza
83
+ doc = Nokogiri::XML::Document.new
84
+ stream = Nokogiri::XML::Node.new("stream:stream", doc)
85
+ doc.add_child(stream)
86
+ stream["xmlns"] = stream_namespace
87
+ stream["xmlns:stream"] = "http://etherx.jabber.org/streams"
88
+ stream["to"] = jid.split("/").first.split("@").last
89
+ stream["version"] = "1.0"
90
+ paste_content_here = Nokogiri::XML::Node.new("paste_content_here", doc)
91
+ stream.add_child(paste_content_here)
92
+ doc.to_xml.split('<paste_content_here/>').first
93
+ end
94
+
95
+ ##
96
+ # Connection_completed is called when the connection (socket) has been established and is in charge of "building" the XML stream
97
+ # to establish the XMPP connection itself.
98
+ # We use a "tweak" here to send only the starting tag of stream:stream
99
+ def connection_completed
100
+ super
101
+ send_xml(stream_stanza)
102
+ end
103
+
104
+ ##
105
+ # Called upon stanza reception
106
+ # Marked as connected when the client has been SASLed, authenticated, biund to a resource and when the session has been created
107
+ def receive_stanza(stanza)
108
+ case @state
109
+ when :connected
110
+ super # Can be dispatched
111
+
112
+ when :wait_for_stream_authenticated
113
+ if stanza.name == "stream:stream" && stanza.attributes['id']
114
+ @state = :wait_for_bind
115
+ end
116
+
117
+ when :wait_for_stream
118
+ if stanza.name == "stream:stream" && stanza.attributes['id']
119
+ @state = :wait_for_auth_mechanisms
120
+ end
121
+
122
+ when :wait_for_auth_mechanisms
123
+ if stanza.name == "stream:features"
124
+ if stanza.children.first.name == "starttls"
125
+ doc = Nokogiri::XML::Document.new
126
+ starttls = Nokogiri::XML::Node.new("starttls", doc)
127
+ doc.add_child(starttls)
128
+ starttls["xmlns"] = "urn:ietf:params:xml:ns:xmpp-tls"
129
+ send_xml(starttls.to_s)
130
+ @state = :wait_for_proceed
131
+ elsif stanza.children.first.name == "mechanisms"
132
+ if stanza.children.first.children.map() { |m| m.text }.include? "PLAIN"
133
+ doc = Nokogiri::XML::Document.new
134
+ auth = Nokogiri::XML::Node.new("auth", doc)
135
+ doc.add_child(auth)
136
+ auth['mechanism'] = "PLAIN"
137
+ auth["xmlns"] = "urn:ietf:params:xml:ns:xmpp-sasl"
138
+ auth.content = Base64::encode64([jid, jid.split("@").first, @password].join("\000")).gsub(/\s/, '')
139
+ send_xml(auth.to_s)
140
+ @state = :wait_for_success
141
+ end
142
+ end
143
+ end
144
+
145
+ when :wait_for_success
146
+ if stanza.name == "success" # Yay! Success
147
+ @state = :wait_for_stream_authenticated
148
+ @parser.reset
149
+ send_xml(stream_stanza)
150
+ elsif stanza.name == "failure"
151
+ if stanza.at("bad-auth") || stanza.at("not-authorized")
152
+ raise AuthenticationError
153
+ else
154
+ end
155
+ else
156
+ # Hum Failure...
157
+ end
158
+
159
+ when :wait_for_bind
160
+ if stanza.name == "stream:features"
161
+ if stanza.children.first.name == "bind"
162
+ doc = Nokogiri::XML::Document.new
163
+ # Let's build the binding_iq
164
+ @binding_iq_id = Integer(rand(10000000))
165
+ iq = Nokogiri::XML::Node.new("iq", doc)
166
+ doc.add_child(iq)
167
+ iq["type"] = "set"
168
+ iq["id"] = binding_iq_id.to_s
169
+ bind = Nokogiri::XML::Node.new("bind", doc)
170
+ bind["xmlns"] = "urn:ietf:params:xml:ns:xmpp-bind"
171
+ iq.add_child(bind)
172
+ resource = Nokogiri::XML::Node.new("resource", doc)
173
+ if jid.split("/").size == 2
174
+ resource.content = (@jid.split("/").last)
175
+ else
176
+ resource.content = "skates_client_#{binding_iq_id}"
177
+ end
178
+ bind.add_child(resource)
179
+ send_xml(iq.to_s)
180
+ @state = :wait_for_confirmed_binding
181
+ end
182
+ end
183
+
184
+ when :wait_for_confirmed_binding
185
+ if stanza.name == "iq" && stanza["type"] == "result" && Integer(stanza["id"]) == binding_iq_id
186
+ if stanza.at("jid")
187
+ @jid = stanza.at("jid").text
188
+ end
189
+ # And now, we must initiate the session
190
+ @session_iq_id = Integer(rand(10000))
191
+ doc = Nokogiri::XML::Document.new
192
+ iq = Nokogiri::XML::Node.new("iq", doc)
193
+ doc.add_child(iq)
194
+ iq["type"] = "set"
195
+ iq["id"] = session_iq_id.to_s
196
+ session = Nokogiri::XML::Node.new("session", doc)
197
+ session["xmlns"] = "urn:ietf:params:xml:ns:xmpp-session"
198
+ iq.add_child(session)
199
+ send_xml(iq.to_s)
200
+ @state = :wait_for_confirmed_session
201
+ end
202
+
203
+ when :wait_for_confirmed_session
204
+ if stanza.name == "iq" && stanza["type"] == "result" && Integer(stanza["id"]) == session_iq_id
205
+ # And now, send a presence!
206
+ doc = Nokogiri::XML::Document.new
207
+ presence = Nokogiri::XML::Node.new("presence", doc)
208
+ send_xml(presence.to_s)
209
+ begin
210
+ @handler.on_connected(self) if @handler and @handler.respond_to?("on_connected")
211
+ rescue
212
+ Skates.logger.error {
213
+ "on_connected failed : #{$!}\n#{$!.backtrace.join("\n")}"
214
+ }
215
+ end
216
+ @state = :connected
217
+ end
218
+
219
+ when :wait_for_proceed
220
+ start_tls() # starting TLS
221
+ @state = :wait_for_stream
222
+ @parser.reset
223
+ send_xml stream_stanza
224
+ end
225
+ end
226
+
227
+ ##
228
+ # Namespace of the client
229
+ def stream_namespace
230
+ "jabber:client"
231
+ end
232
+
233
+ end
234
+ end
@@ -0,0 +1,114 @@
1
+ module Skates
2
+ ##
3
+ # ComponentConnection is in charge of the XMPP connection itself.
4
+ # Upon stanza reception, and depending on the status (connected... etc), this component will handle or forward the stanzas.
5
+ class ComponentConnection < XmppConnection
6
+
7
+ def self.connect(params, handler)
8
+ params["host"] ||= params["jid"]
9
+ super(params, handler)
10
+ end
11
+
12
+ ##
13
+ # Creates a new ComponentConnection and waits for data in the stream
14
+ def initialize(params)
15
+ super(params)
16
+ @state = :wait_for_stream
17
+ end
18
+
19
+ ##
20
+ # Connection_completed is called when the connection (socket) has been established and is in charge of "building" the XML stream
21
+ # to establish the XMPP connection itself.
22
+ # We use a "tweak" here to send only the starting tag of stream:stream
23
+ def connection_completed
24
+ super
25
+ doc = Nokogiri::XML::Document.new
26
+ stream = Nokogiri::XML::Node.new("stream:stream", doc)
27
+ stream["xmlns"] = stream_namespace
28
+ stream["xmlns:stream"] = "http://etherx.jabber.org/streams"
29
+ stream["to"] = jid
30
+ doc.add_child(stream)
31
+ paste_content_here= Nokogiri::XML::Node.new("paste_content_here", doc)
32
+ stream.add_child(paste_content_here)
33
+ start, stop = doc.to_xml.split('<paste_content_here/>')
34
+ send_xml(start)
35
+ end
36
+
37
+ ##
38
+ # XMPP Component handshake as defined in XEP-0114:
39
+ # http://xmpp.org/extensions/xep-0114.html
40
+ def receive_stanza(stanza)
41
+ case @state
42
+ when :connected # Most frequent case
43
+ super(stanza) # Can be dispatched
44
+
45
+ when :wait_for_stream
46
+ if stanza.name == "stream:stream" && stanza.attributes['id']
47
+ # This means the XMPP session started!
48
+ # We must send the handshake now.
49
+ send_xml(handshake(stanza))
50
+ @state = :wait_for_handshake
51
+ else
52
+ raise
53
+ end
54
+
55
+ when :wait_for_handshake
56
+ if stanza.name == "handshake"
57
+ begin
58
+ @handler.on_connected(self) if @handler and @handler.respond_to?("on_connected")
59
+ rescue
60
+ Skates.logger.error {
61
+ "on_connected failed : #{$!}\n#{$!.backtrace.join("\n")}"
62
+ }
63
+ end
64
+ @state = :connected
65
+ elsif stanza.name == "stream:error"
66
+ raise AuthenticationError
67
+ else
68
+ raise
69
+ end
70
+
71
+ end
72
+ end
73
+
74
+ ##
75
+ # Namespace of the component
76
+ def stream_namespace
77
+ 'jabber:component:accept'
78
+ end
79
+
80
+ ##
81
+ # Resolution for Components, based on SRV records
82
+ def self.resolve(host, &block)
83
+ Resolv::DNS.open { |dns|
84
+ # If ruby version is too old and SRV is unknown, this will raise a NameError
85
+ # which is caught below
86
+ Skates.logger.debug {
87
+ "RESOLVING: #{host} "
88
+ }
89
+ found = false
90
+ records = dns.getresources(host, Resolv::DNS::Resource::IN::A)
91
+ records.each do |record|
92
+ ip = record.address.to_s
93
+ if block.call({"host" => ip})
94
+ found = true
95
+ break
96
+ end
97
+ end
98
+ block.call(false) unless found # bleh, we couldn't resolve to any valid. Too bad.
99
+ }
100
+ end
101
+
102
+ private
103
+
104
+ def handshake(stanza)
105
+ hash = Digest::SHA1::hexdigest(stanza.attributes['id'].content + @password)
106
+ doc = Nokogiri::XML::Document.new
107
+ handshake = Nokogiri::XML::Node.new("handshake", doc)
108
+ doc.add_child(handshake)
109
+ handshake.content = hash
110
+ handshake
111
+ end
112
+
113
+ end
114
+ end