radamanthus-skates 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +113 -0
- data/Rakefile +142 -0
- data/bin/skates +8 -0
- data/lib/skates.rb +154 -0
- data/lib/skates/base/controller.rb +116 -0
- data/lib/skates/base/stanza.rb +44 -0
- data/lib/skates/base/view.rb +59 -0
- data/lib/skates/client_connection.rb +234 -0
- data/lib/skates/component_connection.rb +114 -0
- data/lib/skates/ext/array.rb +21 -0
- data/lib/skates/generator.rb +142 -0
- data/lib/skates/router.rb +123 -0
- data/lib/skates/router/dsl.rb +48 -0
- data/lib/skates/runner.rb +164 -0
- data/lib/skates/xmpp_connection.rb +216 -0
- data/lib/skates/xmpp_parser.rb +112 -0
- data/spec/bin/skates_spec.rb +0 -0
- data/spec/em_mock.rb +42 -0
- data/spec/lib/skates/base/controller_spec.rb +205 -0
- data/spec/lib/skates/base/stanza_spec.rb +120 -0
- data/spec/lib/skates/base/view_spec.rb +105 -0
- data/spec/lib/skates/client_connection_spec.rb +309 -0
- data/spec/lib/skates/component_connection_spec.rb +144 -0
- data/spec/lib/skates/generator_spec.rb +10 -0
- data/spec/lib/skates/router/dsl_spec.rb +46 -0
- data/spec/lib/skates/router_spec.rb +252 -0
- data/spec/lib/skates/runner_spec.rb +233 -0
- data/spec/lib/skates/xmpp_connection_spec.rb +222 -0
- data/spec/lib/skates/xmpp_parser_spec.rb +283 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +37 -0
- data/test/skates_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +125 -0
@@ -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
|