skates 0.1.11

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.
Files changed (47) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +113 -0
  3. data/Rakefile +143 -0
  4. data/bin/skates +6 -0
  5. data/lib/skates.rb +108 -0
  6. data/lib/skates/base/controller.rb +116 -0
  7. data/lib/skates/base/stanza.rb +23 -0
  8. data/lib/skates/base/view.rb +58 -0
  9. data/lib/skates/client_connection.rb +210 -0
  10. data/lib/skates/component_connection.rb +87 -0
  11. data/lib/skates/generator.rb +139 -0
  12. data/lib/skates/router.rb +101 -0
  13. data/lib/skates/router/dsl.rb +61 -0
  14. data/lib/skates/runner.rb +137 -0
  15. data/lib/skates/xmpp_connection.rb +172 -0
  16. data/lib/skates/xmpp_parser.rb +117 -0
  17. data/lib/skates/xpath_helper.rb +13 -0
  18. data/spec/bin/babylon_spec.rb +0 -0
  19. data/spec/em_mock.rb +42 -0
  20. data/spec/lib/babylon/base/controller_spec.rb +205 -0
  21. data/spec/lib/babylon/base/stanza_spec.rb +15 -0
  22. data/spec/lib/babylon/base/view_spec.rb +92 -0
  23. data/spec/lib/babylon/client_connection_spec.rb +304 -0
  24. data/spec/lib/babylon/component_connection_spec.rb +135 -0
  25. data/spec/lib/babylon/generator_spec.rb +10 -0
  26. data/spec/lib/babylon/router/dsl_spec.rb +72 -0
  27. data/spec/lib/babylon/router_spec.rb +189 -0
  28. data/spec/lib/babylon/runner_spec.rb +213 -0
  29. data/spec/lib/babylon/xmpp_connection_spec.rb +197 -0
  30. data/spec/lib/babylon/xmpp_parser_spec.rb +275 -0
  31. data/spec/lib/babylon/xpath_helper_spec.rb +25 -0
  32. data/spec/spec_helper.rb +34 -0
  33. data/templates/skates/app/controllers/controller.rb +7 -0
  34. data/templates/skates/app/stanzas/stanza.rb +6 -0
  35. data/templates/skates/app/views/view.rb +6 -0
  36. data/templates/skates/config/boot.rb +16 -0
  37. data/templates/skates/config/config.yaml +24 -0
  38. data/templates/skates/config/dependencies.rb +1 -0
  39. data/templates/skates/config/routes.rb +22 -0
  40. data/templates/skates/log/development.log +0 -0
  41. data/templates/skates/log/production.log +0 -0
  42. data/templates/skates/log/test.log +0 -0
  43. data/templates/skates/script/component +36 -0
  44. data/templates/skates/tmp/pids/README +2 -0
  45. data/test/skates_test.rb +7 -0
  46. data/test/test_helper.rb +10 -0
  47. metadata +160 -0
@@ -0,0 +1,23 @@
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
+ #
7
+ # You can define your own macthing pretty easily with the element and elements methods, as explained in the SaxMachine Documentation: http://github.com/pauldix/sax-machine/tree/master
8
+ # if your stanza is a message stanza, you can match the following for example:
9
+ # element :message, :value => :to, :as => :to
10
+ # element :message, :value => :from, :as => :from
11
+ # element :message, :value => :id, :as => :stanza_id
12
+ # element :message, :value => :type, :as => :stanza_type
13
+ # element :message, :value => :"xml:lang", :as => :lang
14
+ #
15
+ class Stanza
16
+ include SAXMachine
17
+
18
+ def initialize(xml = nil)
19
+ parse(xml.to_s)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,58 @@
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
+ raise ViewFileNotFound, "No such file #{partial_path}" unless Skates.views[partial_path]
21
+ saved_locals = @locals
22
+ @locals = options[:locals]
23
+ eval(Skates.views[partial_path], binding, partial_path, 1)
24
+ @locals = saved_locals # Re-assign the previous locals to be 'clean'
25
+ end
26
+
27
+ ##
28
+ # Instantiate a new view with the various varibales passed in assigns and the path of the template to render.
29
+ def initialize(path = "", assigns = {})
30
+ @view_template = path
31
+ @locals = {}
32
+ assigns.each do |key, value|
33
+ instance_variable_set(:"@#{key}", value)
34
+ end
35
+ end
36
+
37
+ ##
38
+ # "Loads" the view file, and uses the Nokogiri Builder to build the XML stanzas that will be sent.
39
+ def evaluate
40
+ return if @view_template == ""
41
+ raise ViewFileNotFound, "No such file #{@view_template}" unless Skates.views[@view_template]
42
+ builder = Nokogiri::XML::Builder.new
43
+ builder.stream do |xml|
44
+ eval(Skates.views[@view_template], binding, @view_template, 1)
45
+ end
46
+ builder.doc.root.children # we output the document built
47
+ end
48
+
49
+ ##
50
+ # Used to macth locals variables
51
+ def method_missing(sym, *args, &block)
52
+ raise NameError, "undefined local variable or method `#{sym}' for #{self}" unless @locals[sym]
53
+ @locals[sym]
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,210 @@
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 or port has been specified.
20
+ # In any case, we give priority to the specified host and port.
21
+ def self.connect(params, handler = nil)
22
+ return super(params, handler) if params["host"] && params["port"]
23
+
24
+ begin
25
+ srv = []
26
+ Resolv::DNS.open { |dns|
27
+ # If ruby version is too old and SRV is unknown, this will raise a NameError
28
+ # which is caught below
29
+ host_from_jid = params["jid"].split("/").first.split("@").last
30
+ Skates.logger.debug {
31
+ "RESOLVING: _xmpp-client._tcp.#{host_from_jid} (SRV)"
32
+ }
33
+ srv = dns.getresources("_xmpp-client._tcp.#{host_from_jid}", Resolv::DNS::Resource::IN::SRV)
34
+ }
35
+ # Sort SRV records: lowest priority first, highest weight first
36
+ srv.sort! { |a,b| (a.priority != b.priority) ? (a.priority <=> b.priority) : (b.weight <=> a.weight) }
37
+ # And now, for each record, let's try to connect.
38
+ srv.each { |record|
39
+ begin
40
+ params["host"] = record.target.to_s
41
+ params["port"] = Integer(record.port)
42
+ super(params, handler)
43
+ # Success
44
+ break
45
+ rescue NotConnected
46
+ # Try next SRV record
47
+ end
48
+ }
49
+ rescue NameError
50
+ Skates.logger.debug {
51
+ "Resolv::DNS does not support SRV records. Please upgrade to ruby-1.8.3 or later! \n#{$!} : #{$!.backtrace.join("\n")}"
52
+ }
53
+ end
54
+ end
55
+
56
+ ##
57
+ # Builds the stream stanza for this client
58
+ def stream_stanza
59
+ doc = Nokogiri::XML::Document.new
60
+ stream = Nokogiri::XML::Node.new("stream:stream", doc)
61
+ doc.add_child(stream)
62
+ stream["xmlns"] = stream_namespace
63
+ stream["xmlns:stream"] = "http://etherx.jabber.org/streams"
64
+ stream["to"] = jid.split("/").first.split("@").last
65
+ stream["version"] = "1.0"
66
+ paste_content_here = Nokogiri::XML::Node.new("paste_content_here", doc)
67
+ stream.add_child(paste_content_here)
68
+ doc.to_xml.split('<paste_content_here/>').first
69
+ end
70
+
71
+ ##
72
+ # Connection_completed is called when the connection (socket) has been established and is in charge of "building" the XML stream
73
+ # to establish the XMPP connection itself.
74
+ # We use a "tweak" here to send only the starting tag of stream:stream
75
+ def connection_completed
76
+ super
77
+ send_xml(stream_stanza)
78
+ end
79
+
80
+ ##
81
+ # Called upon stanza reception
82
+ # Marked as connected when the client has been SASLed, authenticated, biund to a resource and when the session has been created
83
+ def receive_stanza(stanza)
84
+ case @state
85
+ when :connected
86
+ super # Can be dispatched
87
+
88
+ when :wait_for_stream_authenticated
89
+ if stanza.name == "stream:stream" && stanza.attributes['id']
90
+ @state = :wait_for_bind
91
+ end
92
+
93
+ when :wait_for_stream
94
+ if stanza.name == "stream:stream" && stanza.attributes['id']
95
+ @state = :wait_for_auth_mechanisms
96
+ end
97
+
98
+ when :wait_for_auth_mechanisms
99
+ if stanza.name == "stream:features"
100
+ if stanza.at("starttls") # we shall start tls
101
+ doc = Nokogiri::XML::Document.new
102
+ starttls = Nokogiri::XML::Node.new("starttls", doc)
103
+ doc.add_child(starttls)
104
+ starttls["xmlns"] = "urn:ietf:params:xml:ns:xmpp-tls"
105
+ send_xml(starttls.to_s)
106
+ @state = :wait_for_proceed
107
+ elsif stanza.at("mechanisms") # tls is ok
108
+ if stanza.at("mechanisms").children.map() { |m| m.text }.include? "PLAIN"
109
+ doc = Nokogiri::XML::Document.new
110
+ auth = Nokogiri::XML::Node.new("auth", doc)
111
+ doc.add_child(auth)
112
+ auth['mechanism'] = "PLAIN"
113
+ auth["xmlns"] = "urn:ietf:params:xml:ns:xmpp-sasl"
114
+ auth.content = Base64::encode64([jid, jid.split("@").first, @password].join("\000")).gsub(/\s/, '')
115
+ send_xml(auth.to_s)
116
+ @state = :wait_for_success
117
+ end
118
+ end
119
+ end
120
+
121
+ when :wait_for_success
122
+ if stanza.name == "success" # Yay! Success
123
+ @state = :wait_for_stream_authenticated
124
+ @parser.reset
125
+ send_xml(stream_stanza)
126
+ elsif stanza.name == "failure"
127
+ if stanza.at("bad-auth") || stanza.at("not-authorized")
128
+ raise AuthenticationError
129
+ else
130
+ end
131
+ else
132
+ # Hum Failure...
133
+ end
134
+
135
+ when :wait_for_bind
136
+ if stanza.name == "stream:features"
137
+ if stanza.at("bind")
138
+ doc = Nokogiri::XML::Document.new
139
+ # Let's build the binding_iq
140
+ @binding_iq_id = Integer(rand(10000000))
141
+ iq = Nokogiri::XML::Node.new("iq", doc)
142
+ doc.add_child(iq)
143
+ iq["type"] = "set"
144
+ iq["id"] = binding_iq_id.to_s
145
+ bind = Nokogiri::XML::Node.new("bind", doc)
146
+ bind["xmlns"] = "urn:ietf:params:xml:ns:xmpp-bind"
147
+ iq.add_child(bind)
148
+ resource = Nokogiri::XML::Node.new("resource", doc)
149
+ if jid.split("/").size == 2
150
+ resource.content = (@jid.split("/").last)
151
+ else
152
+ resource.content = "skates_client_#{binding_iq_id}"
153
+ end
154
+ bind.add_child(resource)
155
+ send_xml(iq.to_s)
156
+ @state = :wait_for_confirmed_binding
157
+ end
158
+ end
159
+
160
+ when :wait_for_confirmed_binding
161
+ if stanza.name == "iq" && stanza["type"] == "result" && Integer(stanza["id"]) == binding_iq_id
162
+ if stanza.at("jid")
163
+ @jid = stanza.at("jid").text
164
+ end
165
+ # And now, we must initiate the session
166
+ @session_iq_id = Integer(rand(10000))
167
+ doc = Nokogiri::XML::Document.new
168
+ iq = Nokogiri::XML::Node.new("iq", doc)
169
+ doc.add_child(iq)
170
+ iq["type"] = "set"
171
+ iq["id"] = session_iq_id.to_s
172
+ session = Nokogiri::XML::Node.new("session", doc)
173
+ session["xmlns"] = "urn:ietf:params:xml:ns:xmpp-session"
174
+ iq.add_child(session)
175
+ send_xml(iq.to_s)
176
+ @state = :wait_for_confirmed_session
177
+ end
178
+
179
+ when :wait_for_confirmed_session
180
+ if stanza.name == "iq" && stanza["type"] == "result" && Integer(stanza["id"]) == session_iq_id
181
+ # And now, send a presence!
182
+ doc = Nokogiri::XML::Document.new
183
+ presence = Nokogiri::XML::Node.new("presence", doc)
184
+ send_xml(presence.to_s)
185
+ begin
186
+ @handler.on_connected(self) if @handler and @handler.respond_to?("on_connected")
187
+ rescue
188
+ Skates.logger.error {
189
+ "on_connected failed : #{$!}\n#{$!.backtrace.join("\n")}"
190
+ }
191
+ end
192
+ @state = :connected
193
+ end
194
+
195
+ when :wait_for_proceed
196
+ start_tls() # starting TLS
197
+ @state = :wait_for_stream
198
+ @parser.reset
199
+ send_xml stream_stanza
200
+ end
201
+ end
202
+
203
+ ##
204
+ # Namespace of the client
205
+ def stream_namespace
206
+ "jabber:client"
207
+ end
208
+
209
+ end
210
+ end
@@ -0,0 +1,87 @@
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
+ ##
8
+ # Creates a new ComponentConnection and waits for data in the stream
9
+ def initialize(params)
10
+ super(params)
11
+ @state = :wait_for_stream
12
+ end
13
+
14
+ ##
15
+ # Connection_completed is called when the connection (socket) has been established and is in charge of "building" the XML stream
16
+ # to establish the XMPP connection itself.
17
+ # We use a "tweak" here to send only the starting tag of stream:stream
18
+ def connection_completed
19
+ super
20
+ doc = Nokogiri::XML::Document.new
21
+ stream = Nokogiri::XML::Node.new("stream:stream", doc)
22
+ stream["xmlns"] = stream_namespace
23
+ stream["xmlns:stream"] = "http://etherx.jabber.org/streams"
24
+ stream["to"] = jid
25
+ doc.add_child(stream)
26
+ paste_content_here= Nokogiri::XML::Node.new("paste_content_here", doc)
27
+ stream.add_child(paste_content_here)
28
+ start, stop = doc.to_xml.split('<paste_content_here/>')
29
+ send_xml(start)
30
+ end
31
+
32
+ ##
33
+ # XMPP Component handshake as defined in XEP-0114:
34
+ # http://xmpp.org/extensions/xep-0114.html
35
+ def receive_stanza(stanza)
36
+ case @state
37
+ when :connected # Most frequent case
38
+ super(stanza) # Can be dispatched
39
+
40
+ when :wait_for_stream
41
+ if stanza.name == "stream:stream" && stanza.attributes['id']
42
+ # This means the XMPP session started!
43
+ # We must send the handshake now.
44
+ send_xml(handshake(stanza))
45
+ @state = :wait_for_handshake
46
+ else
47
+ raise
48
+ end
49
+
50
+ when :wait_for_handshake
51
+ if stanza.name == "handshake"
52
+ begin
53
+ @handler.on_connected(self) if @handler and @handler.respond_to?("on_connected")
54
+ rescue
55
+ Skates.logger.error {
56
+ "on_connected failed : #{$!}\n#{$!.backtrace.join("\n")}"
57
+ }
58
+ end
59
+ @state = :connected
60
+ elsif stanza.name == "stream:error"
61
+ raise AuthenticationError
62
+ else
63
+ raise
64
+ end
65
+
66
+ end
67
+ end
68
+
69
+ ##
70
+ # Namespace of the component
71
+ def stream_namespace
72
+ 'jabber:component:accept'
73
+ end
74
+
75
+ private
76
+
77
+ def handshake(stanza)
78
+ hash = Digest::SHA1::hexdigest(stanza.attributes['id'].content + @password)
79
+ doc = Nokogiri::XML::Document.new
80
+ handshake = Nokogiri::XML::Node.new("handshake", doc)
81
+ doc.add_child(handshake)
82
+ handshake.content = hash
83
+ handshake
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,139 @@
1
+ module Skates
2
+ module Generator
3
+ extend Templater::Manifold
4
+
5
+ desc <<-DESC
6
+ Skates is a framework to generate XMPP Applications in Ruby."
7
+ DESC
8
+
9
+ ##
10
+ # Generates a Skates Application
11
+ class ApplicationGenerator < Templater::Generator
12
+ desc <<-DESC
13
+ Generates the file architecture for a Skates Application. To run, you MUST provide an application name"
14
+ DESC
15
+
16
+ first_argument :application_name, :required => true, :desc => "Your application name."
17
+
18
+ def self.source_root
19
+ File.join(File.dirname(__FILE__), '../../templates/skates')
20
+ end
21
+
22
+ # Create all subsdirectories
23
+ empty_directory :app_directory do |d|
24
+ d.destination = "#{application_name}/app"
25
+ end
26
+ empty_directory :controllers_directory do |d|
27
+ d.destination = "#{application_name}/app/controllers"
28
+ end
29
+ empty_directory :views_directory do |d|
30
+ d.destination = "#{application_name}/app/views"
31
+ end
32
+ empty_directory :views_directory do |d|
33
+ d.destination = "#{application_name}/app/stanzas"
34
+ end
35
+ empty_directory :models_directory do |d|
36
+ d.destination = "#{application_name}/app/models"
37
+ end
38
+ empty_directory :initializers_directory do |d|
39
+ d.destination = "#{application_name}/config/initializers"
40
+ end
41
+ empty_directory :tmp_directory do |d|
42
+ d.destination = "#{application_name}/tmp"
43
+ end
44
+ empty_directory :log_directory do |d|
45
+ d.destination = "#{application_name}/log"
46
+ end
47
+ empty_directory :pid_directory do |d|
48
+ d.destination = "#{application_name}/tmp/pids"
49
+ end
50
+
51
+ # And now add the critical files
52
+ file :boot_file do |f|
53
+ f.source = "#{source_root}/config/boot.rb"
54
+ f.destination = "#{application_name}/config/boot.rb"
55
+ end
56
+ file :config_file do |f|
57
+ f.source = "#{source_root}/config/config.yaml"
58
+ f.destination = "#{application_name}/config/config.yaml"
59
+ end
60
+ file :dependencies_file do |f|
61
+ f.source = "#{source_root}/config/dependencies.rb"
62
+ f.destination = "#{application_name}/config/dependencies.rb"
63
+ end
64
+ file :dependencies_file do |f|
65
+ f.source = "#{source_root}/config/routes.rb"
66
+ f.destination = "#{application_name}/config/routes.rb"
67
+ end
68
+ template :component_file do |f|
69
+ f.source = "#{source_root}/script/component"
70
+ f.destination = "#{application_name}/script/component"
71
+ end
72
+
73
+ end
74
+
75
+ ##
76
+ # Generates a new controller, with the corresponding stanzas and routes.
77
+ class ControllerGenerator < Templater::Generator
78
+ desc <<-DESC
79
+ Generates a new controller for the current Application. It also adds the corresponding routes and actions, based on a Xpath and priority. \nSyntax: skates controller <controller_name> [<action_name>:<priority>:<xpath>],[...]"
80
+ DESC
81
+
82
+ first_argument :controller_name, :required => true, :desc => "Name of the Controller."
83
+ second_argument :actions_arg, :required => true, :as => :array, :default => [], :desc => "Actions implemented by this controller. Use the following syntax : name:priority:xpath"
84
+
85
+ def self.source_root
86
+ File.join(File.dirname(__FILE__), '../../templates/skates/app/controllers')
87
+ end
88
+
89
+ def controller_actions
90
+ @controller_actions ||= actions_arg.map { |a| a.split(":") }
91
+ end
92
+
93
+ def controller_class_name
94
+ "#{controller_name.capitalize}Controller"
95
+ end
96
+
97
+ ##
98
+ # This is a hack since Templater doesn't offer any simple way to edit files right now...
99
+ def add_route_for_actions_in_controller(actions, controller)
100
+ sentinel = "Skates.router.draw do"
101
+ router_path = "config/routes.rb"
102
+ actions.each do |action|
103
+ to_inject = "xpath(\"#{action[2]}\").to(:controller => \"#{controller}\", :action => \"#{action[0]}\").priority(#{action[1]})"
104
+ if File.exist?(router_path)
105
+ content = File.read(router_path).gsub(/(#{Regexp.escape(sentinel)})/mi){|match| "#{match}\n\t#{to_inject}"}
106
+ File.open(router_path, 'wb') { |file| file.write(content) }
107
+ end
108
+ end
109
+ end
110
+
111
+ template :controller do |t|
112
+ t.source = "#{source_root}/controller.rb"
113
+ t.destination = "app/controllers/#{controller_name}_controller.rb"
114
+ self.add_route_for_actions_in_controller(controller_actions, controller_name)
115
+ # This is a hack since Templater doesn't offer any simple way to write several files from one...
116
+ FileUtils.mkdir("app/views/#{controller_name}") unless File.exists?("app/views/#{controller_name}")
117
+ controller_actions.each do |action|
118
+ FileUtils.cp("#{source_root}/../views/view.rb", "app/views/#{controller_name}/#{action[0]}.xml.builder")
119
+ end
120
+
121
+ # And now, let's create the stanza files
122
+ controller_actions.each do |action|
123
+ FileUtils.cp("#{source_root}/../stanzas/stanza.rb", "app/stanzas/#{action[0]}.rb")
124
+ # We need to replace
125
+ # "class Stanza < Skates::Base::Stanza" with "class #{action[0]} < Skates::Base::Stanza"
126
+ content = File.read("app/stanzas/#{action[0]}.rb").gsub("class Stanza < Skates::Base::Stanza", "class #{action[0].capitalize} < Skates::Base::Stanza")
127
+ File.open("app/stanzas/#{action[0]}.rb", 'wb') { |file| file.write(content) }
128
+ end
129
+ end
130
+ end
131
+
132
+ # The generators are added to the manifold, and assigned the names 'wiki' and 'blog'.
133
+ # So you can call them <script name> blog merb-blog-in-10-minutes and
134
+ # <script name> blog merb-wiki-in-10-minutes, respectively
135
+ add :application, ApplicationGenerator
136
+ add :controller, ControllerGenerator
137
+
138
+ end
139
+ end