halcyon 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/AUTHORS +1 -0
  2. data/LICENSE +20 -0
  3. data/README +107 -0
  4. data/Rakefile +8 -6
  5. data/bin/halcyon +3 -204
  6. data/lib/halcyon.rb +55 -42
  7. data/lib/halcyon/application.rb +247 -0
  8. data/lib/halcyon/application/router.rb +86 -0
  9. data/lib/halcyon/client.rb +187 -35
  10. data/lib/halcyon/client/ssl.rb +38 -0
  11. data/lib/halcyon/controller.rb +154 -0
  12. data/lib/halcyon/exceptions.rb +67 -59
  13. data/lib/halcyon/logging.rb +31 -0
  14. data/lib/halcyon/logging/analogger.rb +31 -0
  15. data/lib/halcyon/logging/helpers.rb +37 -0
  16. data/lib/halcyon/logging/log4r.rb +25 -0
  17. data/lib/halcyon/logging/logger.rb +20 -0
  18. data/lib/halcyon/logging/logging.rb +19 -0
  19. data/lib/halcyon/runner.rb +141 -0
  20. data/lib/halcyon/runner/commands.rb +141 -0
  21. data/lib/halcyon/runner/helpers.rb +9 -0
  22. data/lib/halcyon/runner/helpers/command_helper.rb +71 -0
  23. data/spec/halcyon/application_spec.rb +70 -0
  24. data/spec/halcyon/client_spec.rb +63 -0
  25. data/spec/halcyon/controller_spec.rb +68 -0
  26. data/spec/halcyon/halcyon_spec.rb +63 -0
  27. data/spec/halcyon/logging_spec.rb +31 -0
  28. data/spec/halcyon/router_spec.rb +37 -12
  29. data/spec/halcyon/runner_spec.rb +54 -0
  30. data/spec/spec_helper.rb +75 -9
  31. data/support/generators/halcyon/USAGE +0 -0
  32. data/support/generators/halcyon/halcyon_generator.rb +52 -0
  33. data/support/generators/halcyon/templates/README +26 -0
  34. data/support/generators/halcyon/templates/Rakefile +32 -0
  35. data/support/generators/halcyon/templates/app/application.rb +43 -0
  36. data/support/generators/halcyon/templates/config/config.yml +36 -0
  37. data/support/generators/halcyon/templates/config/init/environment.rb +11 -0
  38. data/support/generators/halcyon/templates/config/init/hooks.rb +39 -0
  39. data/support/generators/halcyon/templates/config/init/requires.rb +10 -0
  40. data/support/generators/halcyon/templates/config/init/routes.rb +50 -0
  41. data/support/generators/halcyon/templates/lib/client.rb +77 -0
  42. data/support/generators/halcyon/templates/runner.ru +8 -0
  43. data/support/generators/halcyon_flat/USAGE +0 -0
  44. data/support/generators/halcyon_flat/halcyon_flat_generator.rb +52 -0
  45. data/support/generators/halcyon_flat/templates/README +26 -0
  46. data/support/generators/halcyon_flat/templates/Rakefile +32 -0
  47. data/support/generators/halcyon_flat/templates/app.rb +49 -0
  48. data/support/generators/halcyon_flat/templates/lib/client.rb +17 -0
  49. data/support/generators/halcyon_flat/templates/runner.ru +8 -0
  50. metadata +73 -20
  51. data/lib/halcyon/client/base.rb +0 -261
  52. data/lib/halcyon/client/exceptions.rb +0 -41
  53. data/lib/halcyon/client/router.rb +0 -106
  54. data/lib/halcyon/server.rb +0 -62
  55. data/lib/halcyon/server/auth/basic.rb +0 -107
  56. data/lib/halcyon/server/base.rb +0 -774
  57. data/lib/halcyon/server/exceptions.rb +0 -41
  58. data/lib/halcyon/server/router.rb +0 -103
  59. data/spec/halcyon/error_spec.rb +0 -55
  60. data/spec/halcyon/server_spec.rb +0 -105
@@ -1,41 +0,0 @@
1
- #--
2
- # Created by Matt Todd on 2007-12-14.
3
- # Copyright (c) 2007. All rights reserved.
4
- #++
5
-
6
- #--
7
- # module
8
- #++
9
-
10
- module Halcyon
11
- class Client
12
- class Base
13
- module Exceptions #:nodoc:
14
-
15
- #--
16
- # Exception classes
17
- #++
18
-
19
- Halcyon::Exceptions::HTTP_ERROR_CODES.to_a.each do |http_error|
20
- status, body = http_error
21
- class_eval(
22
- "class #{body.gsub(/( |\-)/,'')} < Halcyon::Exceptions::Base\n"+
23
- " def initialize(s=#{status}, e='#{body}')\n"+
24
- " super\n"+
25
- " end\n"+
26
- "end"
27
- );
28
- end
29
-
30
- #--
31
- # Exception Lookup
32
- #++
33
-
34
- def self.lookup(status)
35
- self.const_get(Halcyon::Exceptions::HTTP_ERROR_CODES[status].gsub(/( |\-)/,''))
36
- end
37
-
38
- end
39
- end
40
- end
41
- end
@@ -1,106 +0,0 @@
1
- #--
2
- # Created by Matt Todd on 2007-12-14.
3
- # Copyright (c) 2007. All rights reserved.
4
- #++
5
-
6
- #--
7
- # module
8
- #++
9
-
10
- module Halcyon
11
- class Client
12
-
13
- # = Reverse Routing
14
- #
15
- # Handles URL generation from route params and action names to complement
16
- # the routing ability in the Server.
17
- #
18
- # == Usage
19
- #
20
- # class Simple < Halcyon::Client::Base
21
- # route do |r|
22
- # r.match('/path/to/match').to(:action => 'do_stuff')
23
- # {:action => 'not_found'} # the default route
24
- # end
25
- # def greet(name)
26
- # get(url_for(__method__, :name => name))
27
- # end
28
- # end
29
- #
30
- # == Default Routes
31
- #
32
- # The default route is selected if and only if no other routes matched the
33
- # action and params supplied as a fallback query to supply. This should
34
- # generate an error in most cases, unless you plan to handle this exception
35
- # specifically.
36
- class Router
37
-
38
- # Retrieves the last value from the +route+ call in Halcyon::Client::Base
39
- # and, if it's a Hash, sets it to +@@default_route+ to designate the
40
- # failover route. If +route+ is not a Hash, though, the internal default
41
- # should be used instead (as the last returned value is probably a Route
42
- # object returned by the +r.match().to()+ call).
43
- #
44
- # Used exclusively internally.
45
- def self.default_to route
46
- @@default_route = route.is_a?(Hash) ? route : {:action => 'not_found'}
47
- end
48
-
49
- # This method performs the param matching and URL generation based on the
50
- # inputs from the +url_for+ method. (Caution: not for the feint hearted.)
51
- def self.route(action, params)
52
- r = nil
53
- @@routes.each do |r|
54
- path, pars = r
55
- if pars[:action] == action
56
- # if the actions match up (a pretty good sign of success), make sure the params match up
57
- if (!pars.empty? && !params.empty? && (/(:#{params.keys.first})/ =~ path).nil?) ||
58
- ((pars.empty? && !params.empty?) || (!pars.empty? && params.empty?))
59
- r = nil
60
- next
61
- else
62
- break
63
- end
64
- end
65
- end
66
-
67
- # make sure a route is returned even if no match is found
68
- if r.nil?
69
- #return default route
70
- @@default_route
71
- else
72
- # params (including action and module if set) for the matching route
73
- path = r[0].dup
74
- # replace all params with the proper placeholder in the path
75
- params.each{|p| path.gsub!(/:#{p[0]}/, p[1]) }
76
- path
77
- end
78
- end
79
-
80
- #--
81
- # Route building methods
82
- #++
83
-
84
- # Sets up the +@@routes+ hash and begins the processing by yielding to the block.
85
- def self.prepare
86
- @@path = nil
87
- @@routes = {}
88
- yield self if block_given?
89
- end
90
-
91
- # Stores the path temporarily in order to put it in the hash table.
92
- def self.match(path)
93
- @@path = path
94
- self
95
- end
96
-
97
- # Adds the final route to the hash table and clears the temporary value.
98
- def self.to(params={})
99
- @@routes[@@path] = params
100
- @@path = nil
101
- self
102
- end
103
-
104
- end
105
- end
106
- end
@@ -1,62 +0,0 @@
1
- #--
2
- # Created by Matt Todd on 2007-12-14.
3
- # Copyright (c) 2007. All rights reserved.
4
- #++
5
-
6
- $:.unshift File.dirname(File.join('..', __FILE__))
7
- $:.unshift File.dirname(__FILE__)
8
-
9
- #--
10
- # dependencies
11
- #++
12
-
13
- %w(rubygems halcyon rack).each {|dep|require dep}
14
- begin
15
- require 'json/ext'
16
- rescue LoadError => e
17
- warn 'Using the Pure Ruby JSON... install the json gem to get faster JSON parsing.'
18
- require 'json/pure'
19
- end
20
-
21
- #--
22
- # module
23
- #++
24
-
25
- module Halcyon
26
-
27
- # = Server Communication and Protocol
28
- #
29
- # Server tries to comply with appropriate HTTP response codes, as found at
30
- # <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html>. However, all
31
- # responses are JSON encoded as the server expects a JSON parser on the
32
- # client side since the server should not be processing requests directly
33
- # through the browser. The server expects the User-Agent to be one of:
34
- # +"User-Agent" => "JSON/1.1.1 Compatible (en-US) Halcyon/0.0.12 Client/0.0.1"+
35
- # +"User-Agent" => "JSON/1.1.1 Compatible"+
36
- # The server also expects to accept application/json and be originated
37
- # from the local host (though this can be overridden).
38
- #
39
- # = Usage
40
- #
41
- # For documentation on using Halcyon, check out the Halcyon::Server::Base and
42
- # Halcyon::Client::Base classes which contain much more usage documentation.
43
- class Server
44
- def self.version
45
- VERSION.join('.')
46
- end
47
-
48
- #--
49
- # module dependencies
50
- #++
51
-
52
- autoload :Base, 'halcyon/server/base'
53
- autoload :Router, 'halcyon/server/router'
54
-
55
- end
56
-
57
- end
58
-
59
- # Loads the Exceptions class first which sets up all the dynamically generated
60
- # exceptions used by the system. Must occur before Base is loaded since Base
61
- # depends on it.
62
- %w(halcyon/server/exceptions).each {|dep|require dep}
@@ -1,107 +0,0 @@
1
- #!/usr/bin/env ruby
2
- #--
3
- # Created by Matt Todd on 2008-01-16.
4
- # Copyright (c) 2008. All rights reserved.
5
- #++
6
-
7
- #--
8
- # module
9
- #++
10
-
11
- module Halcyon
12
- class Server
13
- module Auth
14
-
15
- # = Introduction
16
- #
17
- # The Auth::Basic class provides an alternative to the Server::Base
18
- # class for creating servers with HTTP Basic Authentication built in.
19
- #
20
- # == Usage
21
- #
22
- # In order to provide for HTTP Basic Authentication in your server,
23
- # it would first need to inherit from this class instead of Server::Base
24
- # and then provide a method to check for the existence of the credentials
25
- # and respond accordingly. This looks like the following:
26
- #
27
- # class AuthenticatedApp < Halcyon::Server::Auth::Basic
28
- # def basic_authorization(username, password)
29
- # [username, password] == ['rupert', 'secret']
30
- # end
31
- # # write normal Halcyon server app here
32
- # end
33
- #
34
- # The credentials passed to the +basic_authorization+ method are pulled
35
- # from the appropriate Authorization header value and parsed from the
36
- # base64 values. If no Authorization header value is passed, an exception
37
- # is thrown resulting in the appropriate response to the client.
38
- class Basic < Server::Base
39
-
40
- AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION']
41
-
42
- # Determines the appropriate HTTP Authorization header to refer to when
43
- # plucking out the header for processing.
44
- def authorization_key
45
- @authorization_key ||= AUTHORIZATION_KEYS.detect{|k|@env.has_key?(k)}
46
- end
47
-
48
- alias :_run :run
49
-
50
- # Ensures that the HTTP Authentication header is included, the Basic
51
- # scheme is being used, and the credentials pass the +basic_auth+
52
- # test. If any of these fail, an Unauthorized exception is raised
53
- # (except for non-Basic schemes), otherwise the +route+ is +run+
54
- # normally.
55
- #
56
- # See the documentation for the +basic_auth+ class method for details
57
- # concerning the credentials and action inclusion/exclusion.
58
- def run(route)
59
- # test credentials if the action is one specified to be tested
60
- if ((@@auth[:except].nil? && @@auth[:only].nil?) || # the default is to test if no restrictions
61
- (!@@auth[:only].nil? && @@auth[:only].include?(route[:action].to_sym)) || # but if the action is in the :only directive, test
62
- (!@@auth[:except].nil? && !@@auth[:except].include?(route[:action].to_sym))) # or if the action is not in the :except directive, test
63
-
64
- # make sure there's an authorization header
65
- raise Base::Exceptions::Unauthorized.new unless !authorization_key.nil?
66
-
67
- # make sure the request is via the Basic protocol
68
- scheme = @env[authorization_key].split.first.downcase.to_sym
69
- raise Base::Exceptions::BadRequest.new unless scheme == :basic
70
-
71
- # make sure the credentials pass the test
72
- credentials = @env[authorization_key].split.last.unpack("m*").first.split(':', 2)
73
- raise Base::Exceptions::Unauthorized.new unless @@auth[:method].call(*credentials)
74
- end
75
-
76
- # success, so run the route normally
77
- _run(route)
78
- rescue Halcyon::Exceptions::Base => e
79
- @logger.warn "#{uri} => #{e.error}"
80
- # handles all content error exceptions
81
- @res.status = e.status
82
- {:status => e.status, :body => e.error}
83
- end
84
-
85
- # Provides a way to define a test as well as set limits on what is
86
- # tested for Basic Authorization. This method should be called in the
87
- # definition of the server. A simple example would look like:
88
- #
89
- # class Servr < Halcyon::Server::Auth::Basic
90
- # basic_auth :only => [:grant] do |user, pass|
91
- # # test credentials
92
- # end
93
- # # routes and actions follow...
94
- # end
95
- #
96
- # Two acceptable options include <tt>:only</tt> and <tt>:except</tt>.
97
- def self.basic_auth(options={}, &proc)
98
- instance_eval do
99
- @@auth = options.merge(:method => proc)
100
- end
101
- end
102
-
103
- end
104
-
105
- end
106
- end
107
- end
@@ -1,774 +0,0 @@
1
- #--
2
- # Created by Matt Todd on 2007-12-14.
3
- # Copyright (c) 2007. All rights reserved.
4
- #++
5
-
6
- #--
7
- # dependencies
8
- #++
9
-
10
- %w(logger json).each {|dep|require dep}
11
-
12
- #--
13
- # module
14
- #++
15
-
16
- module Halcyon
17
- class Server
18
-
19
- DEFAULT_OPTIONS = {
20
- :root => Dir.pwd,
21
- :environment => 'none',
22
- :port => 9267,
23
- :host => 'localhost',
24
- :server => Gem.searcher.find('thin').nil? ? 'mongrel' : 'thin',
25
- :pid_file => '/var/run/halcyon.{server}.{app}.{port}.pid',
26
- :log_file => '/var/log/halcyon.{app}.log',
27
- :log_level => 'info',
28
- :log_format => proc{|s,t,p,m|"#{s} [#{t.strftime("%Y-%m-%d %H:%M:%S")}] (#{$$}) #{p} :: #{m}\n"},
29
- # handled internally
30
- :acceptable_requests => [],
31
- :acceptable_remotes => []
32
- }
33
- ACCEPTABLE_REQUESTS = [
34
- # ENV var to check, Regexp the value should match, the status code to return in case of failure, the message with the code
35
- ["HTTP_USER_AGENT", /JSON\/1\.1\.\d+ Compatible( \(en-US\) Halcyon\/(\d+\.\d+\.\d+) Client\/(\d+\.\d+\.\d+))?/, 406, 'Not Acceptable'],
36
- ["CONTENT_TYPE", /application\/json/, 415, 'Unsupported Media Type']
37
- ]
38
- ACCEPTABLE_REMOTES = ['localhost', '127.0.0.1', '0.0.0.0']
39
-
40
- # = Building Halcyon Server Apps
41
- #
42
- # Halcyon apps are actually little servers running on top of Rack instances
43
- # which affords a great deal of simplicity and quickness to both design and
44
- # performance.
45
- #
46
- # Building a Halcyon app consists of defining routes to map all requests
47
- # against in order to designate what functionality handles what specific
48
- # requests, the actual actions (and modules) to actually perform these
49
- # requests, and any extensions or configurations you may need or want for
50
- # your individual needs.
51
- #
52
- # == Inheriting from Halcyon::Server::Base
53
- #
54
- # To begin with, an application would be started by simply defining a class
55
- # that inherits from Halcyon::Server::Base.
56
- #
57
- # class Greeter < Halcyon::Server::Base
58
- # end
59
- #
60
- # Once this task has been completed, routes can be defined.
61
- #
62
- # class Greeter < Halcyon::Server::Base
63
- # route do |r|
64
- # r.match('/hello/:name').to(:action => 'greet')
65
- # {:action => 'not_found'} # default route
66
- # end
67
- # end
68
- #
69
- # Two routes are (effectively) defined here, the first being to watch for
70
- # all requests in the format <tt>/hello/:name</tt> where the word pattern
71
- # is stored and transmitted as the appropriate keys in the params hash.
72
- #
73
- # Once we've got our inputs specified, we can start to handle requests:
74
- #
75
- # class Greeter < Halcyon::Server::Base
76
- # route do |r|
77
- # r.match('/hello/:name').to(:action => 'greet')
78
- # {:action => 'not_found'} # default route
79
- # end
80
- # def greet; {:status=>200, :body=>"Hi #{params[:name]}"}; end
81
- # end
82
- #
83
- # You will notice that we only define the method +greet+ and that it
84
- # returns a Hash object containing a +status+ code and +body+ content.
85
- # This is the most basic way to send data, but if all you're doing is
86
- # replying that the request was successful and you have data to return,
87
- # the method +ok+ (an alias of <tt>standard_response</tt>) with the +body+
88
- # param as its sole parameter is sufficient.
89
- #
90
- #
91
- # def greet; ok("Hi #{params[:name]}"); end
92
- #
93
- # You'll also notice that there's no method called +not_found+; this is
94
- # because it is already defined and behaves almost exactly like the +ok+
95
- # method. We could certainly overwrite +not_found+, but at this point it
96
- # is not necessary.
97
- #
98
- # You should also realize that the second route is not defined. This is
99
- # classified as the default route, the route to follow in the event that no
100
- # route actually matches, so it doesn't need any of the extra path to match
101
- # against.
102
- #
103
- # Lastly, the use of +params+ inside the method is simply a method call
104
- # to a hash of the parameters gleaned from the route, such as +:name+ or
105
- # any other variables passed to it.
106
- #
107
- # == The Filesystem
108
- #
109
- # It's important to note that the +halcyon+ commandline tool expects to
110
- # find your server inheriting +Halcyon::Server::Base+ with the same exact
111
- # name as its filename, though with special rules.
112
- #
113
- # To clarify, when your server is stored in +app_server.rb+, it expects
114
- # that your server's class name be +AppServer+ as it capitalizes each word
115
- # and removes all underscores, etc.
116
- #
117
- # Keep this in mind when naming your class and your file, though this
118
- # restriction is only temporary.
119
- #
120
- # NOTE: This really isn't a necessary step if you write your own deployment
121
- # script instead of using the +halcyon+ commandline tool (as it is simply
122
- # a convenience tool). In such, feel free to name your server however you
123
- # prefer and the file likewise.
124
- #
125
- # == Running Your Server On Your Own
126
- #
127
- # If you're wanting to run your server without the help of the +halcyon+
128
- # commandline tool, you will simply need to initialize the server as you
129
- # pass it to the Rack handler of choice along with any configuration
130
- # options you desire.
131
- #
132
- # The following should be enough:
133
- #
134
- # Rack::Handler::Mongrel.run YourAppName.new(options), :Port => 9267
135
- #
136
- # Of course Halcyon already handles most of your dependencies for you, so
137
- # don't worry about requiring Rack, et al. And again, the options are not
138
- # mandatory as the default options are certainly acceptable.
139
- #
140
- # NOTE: If you want to provide debugging information, just set +$debug+ to
141
- # +true+ and you should receive all the debugging information available.
142
- class Base
143
-
144
- #--
145
- # Request Handling
146
- #++
147
-
148
- # = Handling Calls
149
- #
150
- # Receives the request, handles the route matching, runs the approriate
151
- # action based on the route determined (or defaulted to) and finishes by
152
- # responding to the client with the content returned.
153
- #
154
- # == Response and Output
155
- #
156
- # Halcyon responds in purely JSON format (except perhaps on sever server
157
- # malfunctions that aren't caught or intended; read: bugs).
158
- #
159
- # The standard response is simply a JSON-encoded hash following this
160
- # format:
161
- #
162
- # {:status => http_status_code, :body => response_body}
163
- #
164
- # Response body can be any object desired (as long as there is a
165
- # +to_json+ method for it, which includes most core classes), usually
166
- # containing a nested hash with appropriate data.
167
- #
168
- # DO NOT try to call +to_json+ on the +body+ contents as this will cause
169
- # errors when trying to parse JSON.
170
- #
171
- # == Request and Response
172
- #
173
- # If you need access to the Request and Response, the instance variables
174
- # +@req+ and +@res+ will be sufficient for you.
175
- #
176
- # If you need specific documentation for these objects, check the
177
- # corresponding docs in the Rack documentation.
178
- #
179
- # == Requests and POST Data
180
- #
181
- # Most of your requests will have all the data it needs inside of the
182
- # +params+ you receive for your action, but for POST and PUT requests
183
- # (you are being RESTful, right?) you will need to retrieve your data
184
- # from the method +post+. Here's how:
185
- #
186
- # post[:key] => "value"
187
- #
188
- # As you can see, keys specifically are symbols and values as well. What
189
- # this means is that your POST data that you send to the server needs to
190
- # be careful to provide a flat Hash (if anything other than a Hash is
191
- # passed, it is packed up into a hash similar to +{:body=>data}+) or at
192
- # least send a complicated structure as a JSON object so that transport
193
- # is clean. Resurrecting the object is still on your end for POST data
194
- # (though this could change). Here's how you would reconstruct your
195
- # special hash:
196
- #
197
- # value = JSON.parse(post[:key])
198
- #
199
- # That will take care of reconstructing your Hash.
200
- #
201
- # And that is essentially all you need to worry about for retreiving your
202
- # POST contents. Sending POST contents should be documented well enough
203
- # in Halcyon::Client::Base.
204
- #
205
- # == Logging
206
- #
207
- # Logging can be done by logging to +@logger+ when inside the scope of
208
- # application instance (inside of your instance methods and modules).
209
- #
210
- # The +@env+ instance variable has been modified to include a
211
- # +halcyon.logger+ property including the given logger. Use this for
212
- # logging if you need to step outside of the scope of the current
213
- # application instance (just be sure to pass @env along with you).
214
- def call(env)
215
- @time_started = Time.now
216
-
217
- # collect env information, create request and response objects, prep for dispatch
218
- # puts env.inspect if $debug # request information (huge)
219
- @env = env
220
- @res = Rack::Response.new
221
- @req = Rack::Request.new(env)
222
-
223
- # add the logger to the @env instance variable for global access if for
224
- # some reason the environment needs to be passed outside of the
225
- # instance
226
- @env['halcyon.logger'] = @logger
227
-
228
- # pre run hook
229
- before_run(Time.now - @time_started) if respond_to? :before_run
230
-
231
- # prepare route and provide it for callers
232
- route = Router.route(@env)
233
- @env['halcyon.route'] = route
234
-
235
- # dispatch
236
- @res.write(run(route).to_json)
237
-
238
- # post run hook
239
- after_run(Time.now - @time_started) if respond_to? :after_run
240
-
241
- @time_finished = Time.now - @time_started
242
-
243
- # logs access in the following format: [200] / => index (0.0029s;343.79req/s)
244
- req_time, req_per_sec = ((@time_finished*1e4).round.to_f/1e4), (((1.0/@time_finished)*1e2).round.to_f/1e2)
245
- @logger.info "[#{@res.status}] #{@env['REQUEST_URI']} => #{route[:module].to_s}#{((route[:module].nil?) ? "" : "::")}#{route[:action]} (#{req_time}s;#{req_per_sec}req/s)"
246
-
247
- # finish request
248
- @res.finish
249
- end
250
-
251
- # = Dispatching Requests
252
- #
253
- # Dispatches the routed request, handling module resolution and pulling
254
- # all of the param values together for the action. This action is called
255
- # by +call+ and should be transparent to your server app.
256
- #
257
- # One of the design elements of this method is that it rescues all
258
- # Halcon-specific exceptions (defined innside of ::Base::Exceptions) so
259
- # that a proper JSON response may be rendered by +call+.
260
- #
261
- # With this in mind, it is preferred that, for any errors that should
262
- # result in a given HTTP Response code other than 2xx, an appropriate
263
- # exception should be thrown which is then handled by this method's
264
- # rescue clause.
265
- #
266
- # Refer to the Exceptions module to see a list of available Exceptions.
267
- #
268
- # == Acceptable Requests
269
- #
270
- # Halcyon is a very picky server when dealing with requests, requiring
271
- # that clients match a given remote location, accepting JSON responses,
272
- # and matching a certain User-Agent profile. Unless running in debug
273
- # mode, Halcyon will reject all requests with a 403 Forbidden response
274
- # if these requirements are not met.
275
- #
276
- # This means, while in development and testing, the debug flag must be
277
- # enabled if you intend to perform initial tests through the browser.
278
- #
279
- # These restrictions may appear to be arbitrary, but it is simply a
280
- # measure to prevent a live server running in production mode from being
281
- # assaulted by unacceptable clients which keeps the server performing
282
- # actual functions without concerning itself with non-acceptable clients.
283
- #
284
- # The requirements are defined by the Halcyon::Server constants:
285
- # * +ACCEPTABLE_REQUESTS+: defines the necessary User-Agent and Accept
286
- # headers the client must provide.
287
- # * ACCEPTABLE_REMOTES: defines the acceptable remote origins of
288
- # any request. This is primarily limited to
289
- # only local requests, but can be changed.
290
- #
291
- # Halcyon servers are intended to be run behind other applications and
292
- # primarily only speaking with other apps on the same machine, though
293
- # your specific requirements may differ and change that.
294
- #
295
- # When in debug mode or in testing mode, the request filtering test is
296
- # not fired, so all requests from all User Agents and locations will
297
- # succeed. This is important to know if you plan on testing this specific
298
- # feature while in debugging or testing modes.
299
- #
300
- # == Hooks, Callbacks, and Authentication
301
- #
302
- # There is no Authentication mechanism built in to Halcyon (for the time
303
- # being), but there are hooks and callbacks for you to be able to ensure
304
- # that requests are authenticated, etc.
305
- #
306
- # In order to set up a callback, simply define one of the following
307
- # methods in your app's base class:
308
- # * before_run
309
- # * before_action
310
- # * after_action
311
- # * after_run
312
- #
313
- # This is the exact order in which the callbacks are performed if
314
- # defined. Make use of these methods to monitor incoming and outgoing
315
- # requests.
316
- #
317
- # It is preferred for these methods to throw Exceptions::Base exceptions
318
- # (or one of its inheriters) instead of handling them manually. This
319
- # ensures that the actual action is not run when in fact it shouldn't,
320
- # otherwise you could be allowing unauthenticated users privileged
321
- # information or allowing them to perform destructive actions.
322
- def run(route)
323
- # make sure the request meets our expectations
324
- acceptable_request! unless $debug || $test
325
-
326
- # pull params
327
- @params = route.reject{|key, val| [:action, :module].include? key}
328
- @params.merge!(query_params)
329
-
330
- # pre call hook
331
- before_call if respond_to? :before_call
332
-
333
- # handle module actions differently than non-module actions
334
- if route[:module].nil?
335
- # call action
336
- res = send(route[:action])
337
- else
338
- # call module action
339
- mod = self.dup
340
- mod.instance_eval(&(@@modules[route[:module].to_sym]))
341
- res = mod.send(route[:action])
342
- end
343
-
344
- # after call hook
345
- after_call if respond_to? :after_call
346
-
347
- @params = {}
348
-
349
- res
350
- rescue Halcyon::Exceptions::Base => e
351
- @logger.warn "#{uri} => #{e.error}"
352
- # handles all content error exceptions
353
- @res.status = e.status
354
- {:status => e.status, :body => e.error}
355
- end
356
-
357
- # Tests for acceptable requests if +$debug+ and +$test+ are not set.
358
- def acceptable_request!
359
- @config[:acceptable_requests].each do |req|
360
- raise Halcyon::Exceptions::Base.new(req[2], req[3]) unless @env[req[0]] =~ req[1]
361
- end
362
- raise Exceptions::Forbidden.new unless @config[:acceptable_remotes].member? @env["REMOTE_ADDR"]
363
- end
364
-
365
- #--
366
- # Initialization and setup
367
- #++
368
-
369
- # Called when the Handler gets started and stores the configuration
370
- # options used to start the server.
371
- #
372
- # Feel free to define initialize for your app (which is only called once
373
- # per server instance), just be sure to call +super+.
374
- #
375
- # == PID File
376
- #
377
- # A PID file is created when the server is first initialized with the
378
- # current process ID. Where it is located depends on the default option,
379
- # the config file, the commandline option, and the debug status,
380
- # increasing in precedence in that order.
381
- #
382
- # By default, the PID file is placed in +/var/run/+ and is named
383
- # +halcyon.{server}.{app}.{port}.pid+ where +{server}+ is replaced by the
384
- # running server, +{app}+ is the app name (suffixed with +#debug+ if
385
- # running in debug mode), and +{port}+ being the server port (if there
386
- # are multiple servers running, this helps clarify).
387
- #
388
- # There is an option to numerically label your server via the +{n}+
389
- # value, but this is deprecated and will be removed soon. Using the
390
- # +{port}+ option makes much more sense and creates much more meaning.
391
- def initialize(options = {})
392
- # save configuration options
393
- @config = DEFAULT_OPTIONS.merge(options)
394
- @config[:app] ||= self.class.to_s.downcase
395
-
396
- # apply name options to log_file and pid_file configs
397
- apply_log_and_pid_file_name_options
398
-
399
- # debug and test mode handling
400
- enable_debugging if $debug
401
- enable_testing if $test
402
-
403
- # setup logging
404
- setup_logging unless $debug || $test
405
-
406
- # setup request filtering
407
- setup_request_filters unless $debug || $test
408
-
409
- # create PID file
410
- @pid = File.new(@config[:pid_file].gsub('{n}', server_cluster_number), "w", 0644)
411
- @pid << "#{$$}\n"; @pid.close
412
-
413
- # log existence
414
- @logger.info "PID file created. PID is #{$$}."
415
-
416
- # call startup callback if defined
417
- startup if respond_to? :startup
418
-
419
- # log ready state
420
- @logger.info "Started. Awaiting connectivity. Listening on #{@config[:port]}..."
421
-
422
- # trap signals to die (when killed by the user) gracefully
423
- finalize = Proc.new do
424
- @logger.info "Shutting down #{$$}."
425
- clean_up
426
- exit
427
- end
428
- # http://en.wikipedia.org/wiki/Signal_%28computing%29
429
- %w(INT KILL TERM QUIT HUP).each{|sig|trap(sig, finalize)}
430
-
431
- # listen for USR1 signals and toggle debugging accordingly
432
- trap("USR1") do
433
- if $debug
434
- disable_debugging
435
- else
436
- enable_debugging
437
- end
438
- end
439
- end
440
-
441
- # Closes the logger and deletes the PID file.
442
- def clean_up
443
- # don't try to clean up what's cleaned up already
444
- return if defined? @cleaned_up
445
-
446
- # run shutdown hook if defined
447
- shutdown if respond_to? :shutdown
448
-
449
- # close logger, delete PID file, flag clean state
450
- @logger.close
451
- File.delete(@pid.path) if File.exist?(@pid.path)
452
- @cleaned_up = true
453
- end
454
-
455
- # Retreives the server cluster sequence number for the PID file.
456
- #
457
- # This is deprecated and will be removed soon, probably for the 0.4.0
458
- # release. Use of the +{port}+ value is much more appropriate and
459
- # meaningful.
460
- def server_cluster_number
461
- # if there are no +{n}+ references in the PID file name, then simply
462
- # return 0 as the cluster number. (This is the preferred behavior and
463
- # this test allows the method to fail fast. +{n}+ is deprecated and
464
- # will be removed before 0.4.0 is released.)
465
- return 0.to_s if @config[:pid_file]['{n}'].nil?
466
-
467
- # warn users that they're using a deprecated convention.
468
- warn "Your PID file name contains '{n}' (#{@config[:pid_file]}). This is deprecatd and will be removed by the 0.4.0 release. Use '{port}' instead."
469
-
470
- # counts the number of PID files already existing.
471
- server_count = Dir[@config[:pid_file].gsub('{n}','*')].length
472
- # since the counting starts at 0, if the file with the count exists,
473
- # then one of the lesser number servers isn't running, so check each
474
- # PID file until the one not running is found.
475
- # if no files exist, then 0 will be the count, which won't exist, so
476
- # it will be the default number.
477
- while File.exist?(@config[:pid_file].gsub('{n}',server_count.to_s))
478
- server_count -= 1
479
- end
480
- # return that number.
481
- server_count.to_s
482
- end
483
-
484
- # If the server receives a SIGUSR1 signal it will toggle debugging. This
485
- # method is used to setup logging and the request handling methods for
486
- # debugging.
487
- def enable_debugging
488
- $debug = true
489
-
490
- # set the PID file name to /tmp/ unless PID file already exists
491
- @config[:pid_file] = '/tmp/halcyon.{server}.{app}.{port}.pid' unless defined? @pid
492
- apply_log_and_pid_file_name_options # reapply for {server}, {app}, and {port} to be set
493
-
494
- # setup logger to STDOUT and log entering debugging mode
495
- @logger = Logger.new(STDOUT)
496
- @logger.progname = "#{self.class}#debug"
497
- @logger.level = Logger::DEBUG
498
- @logger.formatter = @config[:log_format]
499
- @logger.info "Entering debugging mode..."
500
- rescue Errno::EACCES
501
- abort "Can't access #{@config[:pid_file]}, try 'sudo #{$0}'"
502
- end
503
-
504
- # This method is used to setup logging and the request handling methods
505
- # for debugging.
506
- def enable_testing
507
- # set the PID file name to /tmp/ unless PID file already exists
508
- @config[:pid_file] = '/tmp/halcyon.testing.{app}.{port}.pid' unless defined? @pid
509
- @config[:log_file] = '/tmp/halcyon.testing.{app}.log'
510
- apply_log_and_pid_file_name_options # reapply for {server}, {app}, and {port} to be set
511
-
512
- # setup logger and log entering testing mode
513
- @logger = Logger.new(@config[:log_file])
514
- @logger.progname = "#{self.class}#test"
515
- @logger.level = Logger::DEBUG
516
- @logger.formatter = @config[:log_format]
517
- @logger.info "Entering testing mode..."
518
-
519
- # make sure we clean up after ourselves since we're in testing mode
520
- at_exit {
521
- clean_up
522
- File.delete(@config[:log_file]) if File.exist?(@config[:log_file])
523
- }
524
- rescue Errno::EACCES
525
- abort "Can't access #{@config[:pid_file]}, try 'sudo #{$0}'"
526
- end
527
-
528
- # Disables all of the affects of debugging mode and returns logging and
529
- # request filtering back to normal.
530
- #
531
- # Refer to +enable_debugging+ for more information.
532
- def disable_debugging
533
- # disable logging and log leaving debugging mode
534
- $debug = false
535
- @logger.info "Leaving debugging mode."
536
-
537
- # setup normal logging
538
- setup_logging
539
-
540
- # reenable request filtering
541
- setup_request_filters
542
- end
543
-
544
- # Sets up logging based on the configuration options in +@config+, which
545
- # is set (in order of lowest to highest precedence) in the default
546
- # options, in the configuration file provided, on the commandline, and
547
- # debug mode options.
548
- #
549
- # == Levels
550
- #
551
- # The accepted level values are as follows:
552
- #
553
- # * debug
554
- # * info
555
- # * warn
556
- # * error
557
- # * fatal
558
- # * unknown
559
- #
560
- # These are the exact way you can refer to the logger level you'd like to
561
- # log at from all points of option specification (listed above in order
562
- # of ascending precedence).
563
- #
564
- # If a bogus value is entered, a warning will be issued and the value
565
- # will be defaulted to 'debug'. (So don't mess up.)
566
- def setup_logging
567
- # get the logging level based on the name supplied
568
- level = {
569
- 'debug' => Logger::DEBUG,
570
- 'info' => Logger::INFO,
571
- 'warn' => Logger::WARN,
572
- 'error' => Logger::ERROR,
573
- 'fatal' => Logger::FATAL,
574
- 'unknown' => Logger::UNKNOWN # wtf?
575
- }[@config[:log_level]]
576
- if level.nil?
577
- warn "Logging level specified not acceptable. Defaulting to 'debug'. Check the documentation for the acceptable values."
578
- @config[:log_level] = 'debug'
579
- level = Logger::DEBUG
580
- end
581
-
582
- # setup the logger
583
- @logger = Logger.new(@config[:log_file])
584
- @logger.progname = self.class
585
- @logger.level = level
586
- @logger.formatter = @config[:log_format]
587
- rescue Errno::EACCES
588
- abort "Can't access #{@config[:log_file]}, try 'sudo #{$0}'"
589
- end
590
-
591
- # Sets up request filters based on User-Agent, Content-Type, and Remote
592
- # IP/address values.
593
- #
594
- # Extracted from +initialize+ to reduce repetition.
595
- def setup_request_filters
596
- @config[:acceptable_requests] = ACCEPTABLE_REQUESTS
597
- @config[:acceptable_remotes] = ACCEPTABLE_REMOTES
598
- end
599
-
600
- # Searches through the PID file name and the Log file name stored in the
601
- # +@config+ variable for +{server}+, +{app}+, and +{port}+ values and
602
- # sets them accordingly.
603
- def apply_log_and_pid_file_name_options
604
- # DEFAULT :pid_file => '/var/run/halcyon.{server}.{app}.{port}.pid',
605
- @config[:pid_file].gsub!('{server}', @config[:server])
606
- @config[:pid_file].gsub!('{port}', @config[:port].to_s)
607
- @config[:pid_file].gsub!('{app}', File.basename(@config[:app]))
608
- # DEFAULT :log_file => '/var/log/halcyon.{app}.log',
609
- @config[:log_file].gsub!('{server}', @config[:server])
610
- @config[:log_file].gsub!('{port}', @config[:port].to_s)
611
- @config[:log_file].gsub!('{app}', File.basename(@config[:app]))
612
- end
613
-
614
- # = Routing
615
- #
616
- # Halcyon expects its apps to have routes set up inside of the base class
617
- # (the class that inherits from Halcyon::Server::Base). Routes are
618
- # defined identically to Merb's routes (since Halcyon Router inherits all
619
- # its functionality directly from the Merb Router).
620
- #
621
- # == Usage
622
- #
623
- # A sample Halcyon application defining and handling Routes follows:
624
- #
625
- # class Simple < Halcyon::Server::Base
626
- # route do |r|
627
- # r.match('/user/show/:id').to(:module => 'user', :action => 'show')
628
- # r.match('/hello/:name').to(:action => 'greet')
629
- # r.match('/').to(:action => 'index')
630
- # {:action => 'not_found'} # default route
631
- # end
632
- # user do
633
- # def show(p); ok(p[:id]); end
634
- # end
635
- # def greet(p); ok("Hi #{p[:name]}"); end
636
- # def index(p); ok("..."); end
637
- # def not_found(p); super; end
638
- # end
639
- #
640
- # In this example we define numerous routes for actions and even an
641
- # action in the 'user' module as well as handling the event that no route
642
- # was matched (thereby passing to not_found).
643
- #
644
- # == Modules
645
- #
646
- # A module is simply a named block that whose methods get executed as if
647
- # they were in Base but without conflicting any methods with them, very
648
- # similar to module in Ruby. All that is required to define a module is
649
- # something like this:
650
- #
651
- # admin do
652
- # def users; ok(...); end
653
- # end
654
- #
655
- # This just needs to add one directive when defining what a given route
656
- # maps to, such as:
657
- #
658
- # route do |r|
659
- # r.map('/admin/users').to(:module => 'admin', :action => 'users')
660
- # end
661
- #
662
- # or, alternatively, you can just map to:
663
- #
664
- # r.map('/:module/:action').to()
665
- #
666
- # though it may be better to just explicitly state the module (for
667
- # resolving cleanly when someone starts entering garbage that matches
668
- # incorrectly).
669
- #
670
- # == More Help
671
- #
672
- # In addition to this, you may also find some of the documentation for
673
- # the Router class helpful. However, since the Router is pulled directly
674
- # from Merb, you really should look at the documentation for Merb. You
675
- # can find the documentation on Merb's website at: http://merbivore.com/
676
- def self.route
677
- if block_given?
678
- Router.prepare do |router|
679
- Router.default_to yield(router) || {:action => 'not_found'}
680
- end
681
- else
682
- abort "Halcyon::Server::Base.route expects a block to define routes."
683
- end
684
- end
685
-
686
- # Registers modules internally. (This is designed in a way to prevent
687
- # method naming collisions inside and outside of modules.)
688
- def self.method_missing(name, *params, &proc)
689
- @@modules ||= {}
690
- @@modules[name] = proc
691
- end
692
-
693
- #--
694
- # Properties and shortcuts
695
- #++
696
-
697
- # Takes +msg+ as parameter and formats it into the standard response type
698
- # expected by an action's caller. This format is as follows:
699
- #
700
- # {:status => http_status_code, :body => json_encoded_body}
701
- #
702
- # The methods +standard_response+, +success+, and +ok+ all handle any
703
- # textual message and puts it in the body field, defaulting to the 200
704
- # response class status code.
705
- def standard_response(body = 'OK')
706
- {:status => 200, :body => body}
707
- end
708
- alias_method :success, :standard_response
709
- alias_method :ok, :standard_response
710
-
711
- # Similar to the +standard_response+ method, takes input and responds
712
- # accordingly, which is by raising an exception (which handles formatting
713
- # the response in the normal response hash).
714
- def not_found(body = 'Not Found')
715
- body = 'Not Found' if body.is_a?(Hash) && body.empty?
716
- raise Exceptions::NotFound.new(404, body)
717
- end
718
-
719
- # Returns the params of the current request, set in the +run+ method.
720
- def params
721
- @params
722
- end
723
-
724
- # Returns the params following the ? in a given URL as a hash
725
- def query_params
726
- @env['QUERY_STRING'].split(/&/).inject({}){|h,kp| k,v = kp.split(/=/); h[k] = v; h}.symbolize_keys!
727
- end
728
-
729
- # Returns the URI requested
730
- def uri
731
- # special parsing is done to remove the protocol, host, and port that
732
- # some Handlers leave in there. (Fixes inconsistencies.)
733
- URI.parse(@env['REQUEST_URI'] || @env['PATH_INFO']).path
734
- end
735
-
736
- # Returns the Request Method as a lowercase symbol.
737
- #
738
- # One useful situation for this method would be similar to this:
739
- #
740
- # case method
741
- # when :get
742
- # # perform reading operations
743
- # when :post
744
- # # perform updating operations
745
- # when :put
746
- # # perform creating operations
747
- # when :delete
748
- # # perform deleting options
749
- # end
750
- #
751
- # It can also be used in many other cases, like throwing an exception if
752
- # an action is called with an unexpected method.
753
- def method
754
- @env['REQUEST_METHOD'].downcase.to_sym
755
- end
756
-
757
- # Returns the POST data hash, making the keys symbols first.
758
- #
759
- # Use like <tt>post[:post_param]</tt>.
760
- def post
761
- @req.POST.symbolize_keys!
762
- end
763
-
764
- # Returns the GET data hash, making the keys symbols first.
765
- #
766
- # Use like <tt>get[:get_param]</tt>.
767
- def get
768
- @req.GET.symbolize_keys!
769
- end
770
-
771
- end
772
-
773
- end
774
- end