radamanthus-skates 0.3.5

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.
@@ -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