halcyon 0.4.0 → 0.5.0

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