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