mohiam-babylon 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +111 -0
  3. data/Rakefile +145 -0
  4. data/bin/babylon +6 -0
  5. data/lib/babylon.rb +119 -0
  6. data/lib/babylon/base/controller.rb +116 -0
  7. data/lib/babylon/base/stanza.rb +23 -0
  8. data/lib/babylon/base/view.rb +49 -0
  9. data/lib/babylon/client_connection.rb +210 -0
  10. data/lib/babylon/component_connection.rb +87 -0
  11. data/lib/babylon/generator.rb +139 -0
  12. data/lib/babylon/router.rb +103 -0
  13. data/lib/babylon/router/dsl.rb +61 -0
  14. data/lib/babylon/runner.rb +148 -0
  15. data/lib/babylon/xmpp_connection.rb +172 -0
  16. data/lib/babylon/xmpp_parser.rb +111 -0
  17. data/lib/babylon/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 +86 -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/babylon/app/controllers/controller.rb +7 -0
  34. data/templates/babylon/app/stanzas/stanza.rb +6 -0
  35. data/templates/babylon/app/views/view.rb +6 -0
  36. data/templates/babylon/config/boot.rb +16 -0
  37. data/templates/babylon/config/config.yaml +24 -0
  38. data/templates/babylon/config/dependencies.rb +1 -0
  39. data/templates/babylon/config/routes.rb +22 -0
  40. data/templates/babylon/log/development.log +0 -0
  41. data/templates/babylon/log/production.log +0 -0
  42. data/templates/babylon/log/test.log +52 -0
  43. data/templates/babylon/script/component +46 -0
  44. data/templates/babylon/tmp/pids/README +2 -0
  45. data/test/babylon_test.rb +7 -0
  46. data/test/test_helper.rb +10 -0
  47. metadata +179 -0
@@ -0,0 +1,23 @@
1
+ module Babylon
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,49 @@
1
+ module Babylon
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
+ def render(xml, options = {})
17
+ # First, we need to identify the partial file path, based on the @view_template path.
18
+ partial_path = (@view_template.split("/")[0..-2] + options[:partial].split("/")).join("/").gsub(".xml.builder", "") + ".xml.builder"
19
+ raise ViewFileNotFound, "No such file #{partial_path}" unless Babylon.views[partial_path]
20
+ eval(Babylon.views[partial_path], binding, partial_path, 1)
21
+ end
22
+
23
+ ##
24
+ # Instantiate a new view with the various varibales passed in assigns and the path of the template to render.
25
+ def initialize(path = "", assigns = {})
26
+ @view_template = path
27
+
28
+ assigns.each do |key, value|
29
+ # Babylon.logger.debug("assigning @#{key}='#{value}'")
30
+ instance_variable_set(:"@#{key}", value)
31
+ end
32
+ end
33
+
34
+ ##
35
+ # "Loads" the view file, and uses the Nokogiri Builder to build the XML stanzas that will be sent.
36
+ def evaluate
37
+ return if @view_template == ""
38
+
39
+ # Babylon.logger.debug("eval with template #{@view_template} - looking thru #{Babylon.views}")
40
+ raise ViewFileNotFound, "No such file #{@view_template}" unless Babylon.views[@view_template]
41
+ builder = Nokogiri::XML::Builder.new
42
+ builder.stream do |xml|
43
+ eval(Babylon.views[@view_template], binding, @view_template, 1)
44
+ end
45
+ builder.doc.root.children # we output the document built
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,210 @@
1
+ module Babylon
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
+ Babylon.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
+ Babylon.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 = "babylon_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
+ Babylon.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 Babylon
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
+ Babylon.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 Babylon
2
+ module Generator
3
+ extend Templater::Manifold
4
+
5
+ desc <<-DESC
6
+ Babylon is a framework to generate XMPP Applications in Ruby."
7
+ DESC
8
+
9
+ ##
10
+ # Generates a Babylon Application
11
+ class ApplicationGenerator < Templater::Generator
12
+ desc <<-DESC
13
+ Generates the file architecture for a Babylon 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/babylon')
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: babylon 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/babylon/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 = "Babylon.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 < Babylon::Base::Stanza" with "class #{action[0]} < Babylon::Base::Stanza"
126
+ content = File.read("app/stanzas/#{action[0]}.rb").gsub("class Stanza < Babylon::Base::Stanza", "class #{action[0].capitalize} < Babylon::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