monorail 0.0.1

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.
@@ -0,0 +1,132 @@
1
+ # $Id: app.rb 2374 2006-04-24 02:51:49Z francis $
2
+ #
3
+ # Monorail::Application
4
+ # This runs a monorail system from a command-line binary.
5
+ # The binary is generated by rake and invokes us here.
6
+ #
7
+ #--
8
+ # This program is free software; you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation; either version 2 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # This program is free software; you can redistribute it and/or modify
14
+ # it under the terms of the GNU General Public License as published by
15
+ # the Free Software Foundation; either version 2 of the License, or
16
+ # (at your option) any later version.
17
+ #
18
+ # This program is distributed in the hope that it will be useful,
19
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
20
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
+ # GNU General Public License for more details.
22
+ #
23
+ # You should have received a copy of the GNU General Public License
24
+ # along with this program; if not, write to the Free Software
25
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
26
+ #
27
+
28
+
29
+
30
+
31
+ require 'ostruct'
32
+ require 'optparse'
33
+
34
+
35
+ module Monorail
36
+
37
+ # TODO, this is not unified with the version string in the Rakefile.
38
+ Version = "0.0.1"
39
+
40
+ def self.application
41
+ Application.new
42
+ end
43
+
44
+
45
+ class Application
46
+
47
+
48
+ def initialize
49
+ end
50
+
51
+
52
+ def run
53
+ parse_arguments
54
+ Monorail.new( @options ).run
55
+ end
56
+
57
+
58
+ def parse_arguments
59
+ @options = OpenStruct.new
60
+ @options.host = "127.0.0.1"
61
+ @options.port = 8090
62
+ @options.ssl = false
63
+ @options.daemon = false
64
+ @options.rails = nil
65
+ @options.monorail = nil
66
+
67
+ opts = OptionParser.new {|opts|
68
+
69
+ opts.separator ""
70
+ opts.separator "Options summary:"
71
+
72
+ opts.on("-a", "--addr IP-address",
73
+ "Requires the host IP address to listen on",
74
+ " default: 127.0.0.1") {|addr|
75
+ @options.host = addr
76
+ }
77
+
78
+ opts.on("-p", "--port TCP port",
79
+ "Requires the TCP port to listen on",
80
+ " default: 8090") {|port|
81
+ @options.port = port.to_i
82
+ }
83
+
84
+ opts.on("-s", "--[no-]ssl", "Require SSL (HTTPS)",
85
+ " default: false") {|ssl|
86
+ @options.ssl = ssl
87
+ }
88
+
89
+ opts.on("-d", "--[no-]daemon", "Run in the background",
90
+ " default: false") {|d|
91
+ @options.daemon = d
92
+ }
93
+
94
+ # TODO, wrong, we need one parameter to select the environment, not several.
95
+ opts.on("--rails [DIR]", "run a rails app in the specified directory",
96
+ " default: the current directory") {|d|
97
+ @options.rails = d || "."
98
+ }
99
+
100
+ opts.on("--monorail [DIR]", "run a monorails app in the specified directory",
101
+ " default: the current directory") {|d|
102
+ @options.monorail = d || "."
103
+ }
104
+
105
+ # Options summary
106
+ opts.on_tail("-h", "--help", "Show this message") {
107
+ puts opts
108
+ exit
109
+ }
110
+ # Version
111
+ opts.on_tail("--version", "Show version") {
112
+ puts Monorail::Version
113
+ exit
114
+ }
115
+
116
+
117
+ }
118
+
119
+ opts.parse!(ARGV)
120
+
121
+ end
122
+
123
+
124
+
125
+ end # class Application
126
+
127
+
128
+ end # module Monorail
129
+
130
+
131
+ #----------------------
132
+
@@ -0,0 +1,753 @@
1
+ # MONORAIL for WEBD
2
+ # $Id: monorail_webd.rb 2190 2006-04-03 08:49:46Z francis $
3
+ #
4
+ #--
5
+ # This program is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 2 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is free software; you can redistribute it and/or modify
11
+ # it under the terms of the GNU General Public License as published by
12
+ # the Free Software Foundation; either version 2 of the License, or
13
+ # (at your option) any later version.
14
+ #
15
+ # This program is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU General Public License
21
+ # along with this program; if not, write to the Free Software
22
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23
+ #
24
+
25
+
26
+
27
+
28
+ require 'dl/import'
29
+ require 'timeout'
30
+ require 'stringio'
31
+ require 'cgi'
32
+ require 'erb'
33
+ require 'singleton'
34
+ require 'md5'
35
+
36
+
37
+ module Monorail
38
+
39
+
40
+
41
+ #
42
+ # Link in the external library for WEBD (socket-machine handler).
43
+ # And set up the callbacks so we can handle requests here.
44
+ #
45
+ =begin
46
+ extend DL::Importable
47
+ dlload "./libwebd"
48
+
49
+ def request_callback
50
+ m = Request.new
51
+ resp = m.generate_response
52
+ fulfill_webd_request m.http_response_code, resp, resp.length
53
+ end
54
+
55
+ RequestCallback = callback "void request_callback()"
56
+
57
+ extern "void initialize_webd (void*)"
58
+ extern "void fulfill_webd_request (int, const char*, int)"
59
+ extern "const void *retrieve_post_content()"
60
+ =end
61
+
62
+
63
+ #
64
+ # class SessionManager
65
+ # Generate sessions and session keys.
66
+ # This singleton class is used to support Monorail requests.
67
+ # We get our session keys from a system entropy machine.
68
+ # This version REQUIRES uuidgen and throws a fatal error
69
+ # if it's not present.
70
+ #
71
+ class SessionManager
72
+ include Singleton
73
+
74
+ class TooManySessions < Exception; end
75
+
76
+ # TODO, make the session timeout (currently hardcoded to 10 minutes) configurable.
77
+ SessionTimeout = 600
78
+ # Authorization timeout is an interval after which we must re-authorize against
79
+ # an authoritative source. It's to make sure we cut the user off in case his
80
+ # access rights should expire or be revoked during a session.
81
+ AuthorizationTimeout = 120
82
+ # MaxSessions is the top number of sessions we permit at a time.
83
+ # May need to become configurable
84
+ MaxSessions = 100
85
+
86
+ #
87
+ # initialize
88
+ #
89
+ def initialize
90
+ # generate one key and throw it away. That way if there is a problem
91
+ # with the entropy generator, it'll generate an exception at the top
92
+ # of the run.
93
+ generate_key
94
+ @sessions = {}
95
+ end
96
+
97
+
98
+ #
99
+ # get_key_seed
100
+ # This method uses uuidgen, which must be present on the system.
101
+ # Override here to use some other mechanism.
102
+ #
103
+ def get_key_seed
104
+ `uuidgen -r`.chomp.gsub(/[\-]/, "")
105
+ end
106
+
107
+ #
108
+ # generate_key
109
+ #
110
+ def generate_key
111
+ if !@key_suffix or @key_suffix > 1000000
112
+ @key_seed = get_key_seed
113
+ raise "Invalid session-key seed" unless @key_seed.length > 8
114
+ @key_suffix = 0
115
+ end
116
+ @key_suffix += 1
117
+ format( "%s%08x", @key_seed, @key_suffix )
118
+ end
119
+
120
+ #
121
+ # retrieve_or_create_session
122
+ #
123
+ def retrieve_or_create_session session_name
124
+ sc = @sessions[session_name]
125
+
126
+ if sc
127
+ if sc.is_expired?
128
+ @sessions.delete sc.session_name
129
+ sc = nil
130
+ else
131
+ sc.touch
132
+ end
133
+ end
134
+
135
+ sc or create_session
136
+
137
+ end
138
+
139
+
140
+ #
141
+ # create_session
142
+ # This is as a good a place as any to purge expired sessions.
143
+ # We also throttle the total number of sessions open at any given time.
144
+ #
145
+ def create_session
146
+ purge_expired_sessions
147
+
148
+ unless @sessions.size < MaxSessions
149
+ raise TooManySessions
150
+ end
151
+
152
+ sc = Session.new
153
+ @sessions [sc.session_name.dup] = sc
154
+ sc
155
+ end
156
+
157
+
158
+ #
159
+ # purge_expired_sessions
160
+ #
161
+ def purge_expired_sessions
162
+ @sessions.delete_if {|k,v|
163
+ v.is_expired?
164
+ }
165
+ end
166
+
167
+ end
168
+
169
+
170
+ #
171
+ # class Session
172
+ #
173
+ class Session < Hash
174
+
175
+ attr_reader :session_name
176
+ attr_reader :username
177
+ attr_reader :password
178
+
179
+ #
180
+ # initialize
181
+ #
182
+ def initialize
183
+ super()
184
+ @session_name = SessionManager.instance.generate_key
185
+ @first_use = true
186
+ @created_at = Time.now
187
+ @last_retrieved = nil
188
+ end
189
+
190
+ #
191
+ # first_use?
192
+ #
193
+ def first_use?
194
+ @last_retrieved == nil
195
+ end
196
+
197
+ #
198
+ # is_expired?
199
+ #
200
+ def is_expired?
201
+ last_touch = (@last_retrieved or @created_at)
202
+ (Time.now - last_touch) > SessionManager::SessionTimeout
203
+ end
204
+
205
+ #
206
+ # touch
207
+ #
208
+ def touch
209
+ @last_retrieved = Time.now
210
+ end
211
+
212
+ #
213
+ # set_credential
214
+ #
215
+ def set_credential user, psw
216
+ @username,@password = user,psw
217
+ end
218
+
219
+
220
+ #
221
+ # authorize
222
+ #
223
+ def authorize
224
+ @last_authorized = Time.now
225
+ end
226
+
227
+ #
228
+ # unauthorize
229
+ # Used to implement a logout function.
230
+ # Must null out the stored credential, otherwise the authorizer
231
+ # will just go back to the system function and try again.
232
+ # Security risk: Must make sure somehow that these session objects never
233
+ # get swapped out.
234
+ #
235
+ def unauthorize
236
+ @last_authorized = nil
237
+ @username = nil
238
+ @password = nil
239
+ end
240
+
241
+ #
242
+ # authorized?
243
+ #
244
+ def authorized?
245
+ @last_authorized and ((Time.now - @last_authorized) < SessionManager::AuthorizationTimeout)
246
+ end
247
+
248
+ end # class Session
249
+
250
+
251
+ #
252
+ # class Request
253
+ #
254
+ class Request
255
+
256
+ attr_reader :http_response_code
257
+
258
+ class FileNotFound < Exception; end
259
+ class DirectoryBrowsed < Exception; end
260
+ class UndefinedController < Exception; end
261
+ class UndefinedControllerModule < Exception; end
262
+ class TemplateNotFound < Exception; end
263
+ class InvalidVerb < Exception; end
264
+ class Unauthorized < Exception; end
265
+
266
+ #
267
+ # authentication-page parameters
268
+ # These are set by configuration.
269
+ # @@authpage_directory is a real directory on the filesystem
270
+ # containing a monorail controller that generates a login page.
271
+ # @@authpage_controller is the name of the controller to invoke.
272
+ # If either of these is not initialized, then accesses requiring
273
+ # authentication will just fail with a 403 error.
274
+ #
275
+ @@authpage_directory = nil
276
+ @@authpage_controller = nil
277
+ #
278
+ # authentication_page
279
+ # Called by a configurator to specify the real directory and controller name
280
+ # of a single (global) page that will generate an auth challenge.
281
+ def Request::authentication_page actual, controller
282
+ @@authpage_directory,@@authpage_controller = actual,controller
283
+ end
284
+
285
+ #
286
+ # use_sessions
287
+ # This is a class method that sets the class variable @@use_sessions.
288
+ # When true, a session will automatically be created and maintained for
289
+ # each user. It will automatically be made available to controllers
290
+ # and page templates.
291
+ # Sessions may be used without authentication. Authentication REQUIRES sessions.
292
+ # Sessions are NOT used by default.
293
+ # Session cookies are hardcoded here in a class variable. Make it configurable someday.
294
+ #
295
+ @@use_sessions = false
296
+ @@session_cookie_name = "monorail_session"
297
+ def Request::use_sessions sess
298
+ @@use_sessions = sess
299
+ end
300
+
301
+ #
302
+ # authorization_proc
303
+ # This is a proc object which expects two parameters, a user and a password.
304
+ # It must be specified before authorization will work. This permits
305
+ # pluggable auth/az methods.
306
+ #
307
+ @@authorization_proc = nil
308
+ def Request::authorization_proc pr
309
+ @@authorization_proc = pr
310
+ end
311
+
312
+ #
313
+ # mime mappings
314
+ #
315
+ @@mime_mappings = {
316
+ "gif" => "image/gif",
317
+ "jpg" => "image/jpeg",
318
+ "jpeg" => "image/jpeg",
319
+ "txt" => "text/plain",
320
+ "html" => "text/html",
321
+ "js" => "text/javascript",
322
+ "css" => "text/css",
323
+ }
324
+ def Request::mime_mapping suffix, mimetype
325
+ @mime_mappings [suffix] = mimetype
326
+ end
327
+
328
+
329
+ #
330
+ # verbose flag
331
+ # Defaults false, triggers debugging info to stderr.
332
+ # Needs to be configurable.
333
+ #
334
+ @@verbose = false
335
+ def Request::verbose v
336
+ @@verbose = v
337
+ end
338
+
339
+ #
340
+ # debug flag
341
+ # When set, this will select behaviors appropriate for development.
342
+ # For example, controller files will be loaded on every request
343
+ # instead of just required.
344
+ @@debug = false
345
+ def Request::debug d
346
+ @@debug = d
347
+ end
348
+
349
+
350
+
351
+ # class DirectoryAlias
352
+ #
353
+ class DirectoryAlias
354
+
355
+ attr_reader :prefix, :actual, :processor
356
+
357
+ #
358
+ # initialize
359
+ #
360
+ def initialize prefix, actual, processor, auth_required
361
+ unless prefix =~ /^\//
362
+ prefix = "/" + prefix
363
+ end
364
+ unless prefix =~ /\/$/
365
+ prefix = prefix + "/"
366
+ end
367
+ unless actual =~ /\/$/
368
+ actual = actual + "/"
369
+ end
370
+
371
+ @prefix = prefix
372
+ @prefix_pattern = Regexp.new( "^#{prefix}" )
373
+ @actual = actual
374
+ @processor = processor
375
+ @auth_required = auth_required
376
+ end
377
+
378
+ #
379
+ # auth_required?
380
+ #
381
+ def auth_required?
382
+ @auth_required
383
+ end
384
+
385
+ #
386
+ # match_request
387
+ # Does our prefix match that of the incoming path_info?
388
+ # Return T/F
389
+ #
390
+ def match_request path_info
391
+ path_info =~ @prefix_pattern
392
+ end
393
+
394
+ #
395
+ # default_resource
396
+ # Generate a default resource, such as for requests that don't
397
+ # specify one. For now we only handle directory requests, and
398
+ # we hardcode index.html. Eventually this needs to become more
399
+ # flexible.
400
+ def default_resource
401
+ case @processor
402
+ when :directory
403
+ "index.html"
404
+ else
405
+ ""
406
+ end
407
+ end
408
+
409
+
410
+ #
411
+ # translate_pathname
412
+ #
413
+ def translate_pathname path_info
414
+ if path_info =~ @prefix_pattern
415
+ filename = $' || ""
416
+ filename.length > 0 or filename = default_resource
417
+ @actual + filename
418
+ end
419
+ end
420
+
421
+ #
422
+ # get_path_tail
423
+ #
424
+ def get_path_tail path_info
425
+ if path_info =~ @prefix_pattern
426
+ $'
427
+ end
428
+ end
429
+
430
+ end # class DirectoryAlias
431
+ # directory_aliases class variable is an array of DirectoryAlias objects
432
+ @@directory_aliases = []
433
+
434
+
435
+ #
436
+ # Request::directory_alias
437
+ # This is used to specify directories. Could also do it via a config file,
438
+ # that might be better.
439
+ # Each entry consists of a prefix name which is head-matched against incoming
440
+ # HTTP requests, an actual pathname in the filesystem, which should be FQ,
441
+ # a responder type (:directory and :monorail are currently defined),
442
+ # and an authentication-required flag (T/F).
443
+ # There is one oddity: we support a notion of a "default" directory, where
444
+ # the prefix is "/". Now this array of directory aliases is searched in
445
+ # order, but there is a problem with the default directory because a prefix
446
+ # of "/" will head-match any request. So as a special case, we ENSURE here
447
+ # that any such directory will be added to the bottom of the list.
448
+ # Use a sort instead of a tail-inspection because there could be more than
449
+ # one default directory specified (even though that would make no sense).
450
+ #
451
+ def Request::directory_alias prefix, actual, responder, authreq
452
+ @@directory_aliases << DirectoryAlias.new( prefix, actual, responder, authreq )
453
+ @@directory_aliases.sort! {|a,b| ((a.prefix == '/') ? 1 : 0) <=> ((b.prefix == '/') ? 1 : 0) }
454
+ end
455
+
456
+ #
457
+ # initialize
458
+ #
459
+ def initialize
460
+ @headers = {"content-type" => "text/html"}
461
+ @cookies = []
462
+ @http_response_code = 200
463
+ end
464
+
465
+
466
+ #
467
+ # initialize_session
468
+ # We use a HARDCODED cookie name. Revise this later
469
+ # if it's necessary to support multiple applications with
470
+ # distinct sessions.
471
+ # When initializing a CGI::Cookie, DON'T LEAVE OUT THE PATH.
472
+ # It's optional but the default is the current path, not the whole site,
473
+ # so it's not really a session cookie.
474
+ # WARNING, that may not end up being good enough.
475
+ #
476
+ def initialize_session
477
+ return unless @@use_sessions
478
+ sc = @cgi.cookies[@@session_cookie_name] and sc = sc.shift
479
+ @session = SessionManager.instance.retrieve_or_create_session( sc )
480
+ if @session.first_use?
481
+ @cookies << CGI::Cookie::new( 'name' => @@session_cookie_name, 'value' => [@session.session_name], 'path' => "/" )
482
+ end
483
+ end
484
+
485
+
486
+ #
487
+ # generate_content
488
+ #
489
+ def generate_content
490
+ diralias = @@directory_aliases.detect {|da| da.match_request @cgi.path_info }
491
+
492
+ diralias or raise FileNotFound
493
+
494
+ if diralias.auth_required? and !check_authorization
495
+ if @@authpage_directory and @@authpage_controller
496
+ generate_content_from_monorail( @@authpage_directory, @@authpage_controller )
497
+ else
498
+ raise Unauthorized
499
+ end
500
+
501
+ else
502
+ # here, we're either authorized or no auth is required. Gen the page.
503
+ case diralias.processor
504
+ when :directory
505
+ generate_content_from_directory( diralias.translate_pathname( @cgi.path_info ))
506
+ when :monorail
507
+ generate_content_from_monorail( diralias.actual, diralias.get_path_tail( @cgi.path_info ))
508
+ else
509
+ raise FileNotFound
510
+ end
511
+
512
+ end
513
+
514
+ end
515
+
516
+
517
+ #
518
+ # check_authorization
519
+ #
520
+ def check_authorization
521
+ if @session
522
+ if @session.authorized?
523
+ true
524
+ elsif @@authorization_proc and @@authorization_proc.call( @session.username, @session.password )
525
+ @session.authorize
526
+ true
527
+ end
528
+ end
529
+ end
530
+
531
+
532
+ #
533
+ # compute_file_etag
534
+ # We concatenate the inode mtime and size from a filename
535
+ # and MD5-hash them.
536
+ # We expect valid input so throw an exception on error.
537
+ #
538
+ def compute_file_etag filename
539
+ MD5.new( "#{File.mtime( filename ).to_i}::#{File.size( filename )}" ).to_s
540
+ end
541
+
542
+ #
543
+ # generate_content_from_directory
544
+ # We come here to generate content by reading a file on the filesystem.
545
+ #
546
+ def generate_content_from_directory filename
547
+ if filename =~ /[\/]+$/
548
+ @@verbose and $stderr.puts "failing directory request: #{filename}"
549
+ raise DirectoryBrowsed
550
+ end
551
+ if File.exist?(filename) && !File.directory?(filename)
552
+ etag = compute_file_etag( filename )
553
+ if if_none_match = ENV["IF_NONE_MATCH"] and if_none_match == etag
554
+ @@verbose and $stderr.puts "fulfilling directory request (Etag): #{filename}"
555
+ @http_response_code = 304
556
+ ""
557
+ else
558
+ @@verbose and $stderr.puts "fulfilling directory request: #{filename}"
559
+ @headers['ETag'] = etag
560
+ compute_content_type
561
+ File.read filename
562
+ end
563
+ else
564
+ raise FileNotFound
565
+ end
566
+ end
567
+
568
+
569
+ #
570
+ # generate_content_from_monorail
571
+ # We load up a ruby file based on the name in the path_info.
572
+ # OBSERVE: there are no subdirectories. The ruby module needs
573
+ # to be in the top subdirectory under the actual pathname.
574
+ # Any rendered pages will be in subdirectory "pages"
575
+ # which is a security feature. Since we won't let a URL
576
+ # specify a controller file below the top-level, that means
577
+ # the files which define page templates are not accessible
578
+ # by URL.
579
+ # Observe that there is a debug path available when mixin in the
580
+ # controller personality, but it hardcodes that the controller filename
581
+ # end in .rb.
582
+ #
583
+ def generate_content_from_monorail path_prefix, pathname
584
+ @@verbose and $stderr.puts "fulfilling monorail request: #{path_prefix} :: #{pathname}"
585
+ paths = pathname.split(/[\/]+/)
586
+ (action = paths.shift || "index") and action.gsub!(/[\.]/, "_")
587
+
588
+ verb = paths.shift || "index"
589
+
590
+ # Before mixing in the personality, make sure the request verb
591
+ # isn't already present in this class. This prevents people calling
592
+ # things like initialize.
593
+ raise InvalidVerb if self.respond_to?( verb )
594
+
595
+ # load action handler
596
+ begin
597
+ if @@debug
598
+ load File.join( path_prefix, action ) + ".rb"
599
+ else
600
+ require File.join( path_prefix, action )
601
+ end
602
+ instance_eval "extend Controller_#{action}"
603
+ rescue LoadError
604
+ raise UndefinedController
605
+ rescue NameError
606
+ raise UndefinedControllerModule
607
+ end
608
+
609
+ # Now throw something if the requested verb has NOT been mixed in.
610
+ raise InvalidVerb unless self.respond_to?( verb )
611
+
612
+ @headers ['Pragma'] = "no-cache"
613
+ @headers ['Expires'] = "-1"
614
+ @headers ['Cache-control'] = "no-cache"
615
+
616
+ @extra_paths = paths || []
617
+ @path_prefix = File.join( path_prefix, "pages" )
618
+ instance_eval verb
619
+
620
+ end
621
+
622
+
623
+ #
624
+ # read_post_contents
625
+ #
626
+ def read_post_contents
627
+ return unless @cgi.request_method == "POST"
628
+ clen = ENV["POST_CONTENT_LENGTH"] and clen = clen.to_i
629
+ return unless clen && (clen > 0)
630
+
631
+ pc = ::Monorail::module_eval { retrieve_post_content }
632
+ if pc and pc.respond_to?(:to_s)
633
+ pc = pc.to_s( clen )
634
+ if pc and pc.length == clen
635
+ if @cgi.content_type.downcase == "application/x-www-form-urlencoded"
636
+ @cgi.params.merge!( CGI::parse( pc ))
637
+ else
638
+ # We have some kind of possibly binary content.
639
+ # Might be multipart too.
640
+ # Sit on it until we need to do something with it.
641
+ @post_content = pc
642
+ end
643
+ end
644
+ end
645
+
646
+ end
647
+
648
+ #
649
+ # generate_response
650
+ #
651
+ def generate_response
652
+ content = begin
653
+ timeout(3) {
654
+ @cgi = CGI.new
655
+ initialize_session
656
+ read_post_contents
657
+ generate_content
658
+ }
659
+ rescue Timeout::Error
660
+ @http_response_code = 500
661
+ "Timeout Error: #{$!}"
662
+ rescue RuntimeError
663
+ @http_response_code = 500
664
+ "Runtime Error: #{$!.message}"
665
+ rescue FileNotFound, UndefinedController, UndefinedControllerModule, TemplateNotFound, InvalidVerb
666
+ @http_response_code = 404
667
+ $!.message # Add a custom 404 handler here if desired
668
+ rescue DirectoryBrowsed
669
+ @http_response_code = 403
670
+ "Directory browsing not permitted" # Add a custom 403 handler here if desired
671
+ rescue Unauthorized
672
+ @http_response_code = 403
673
+ "Unauthorized" # Add a custom 403 handler here if desired
674
+ rescue SessionManager::TooManySessions
675
+ @http_response_code = 500
676
+ $!
677
+ rescue
678
+ @http_response_code = 500
679
+ "Unspecified Error: #{$!}"
680
+ end
681
+
682
+ ss = StringIO.new
683
+ ss << "HTTP/1.1 #{@http_response_code} ...\r\n"
684
+
685
+ @headers.each {|k,v| ss << "#{k}: #{v}\r\n" }
686
+ @cookies.each {|c| ss << "Set-Cookie: #{c.to_s}\r\n" }
687
+ ss << "Content-length: #{content.to_s.length}\r\n"
688
+ ss << "\r\n"
689
+ ss << content
690
+
691
+ ss.string
692
+ end
693
+
694
+ #
695
+ # compute_content_type
696
+ # This is probably too crude and will need to be modified.
697
+ # We look at the tail of the path_info and infer a mime type.
698
+ # This is really only appropriate for requests fulfilled out
699
+ # of the filesystem. For script-generated responses, they will
700
+ # want to define content-type manually.
701
+ #
702
+ def compute_content_type
703
+ hdr = "text/html"
704
+ path_tail = if @cgi
705
+ if @cgi.path_info =~ /\.([^\.]+)$/i
706
+ $1
707
+ end
708
+ end
709
+ if path_tail and @@mime_mappings.has_key?(path_tail)
710
+ hdr = @@mime_mappings[path_tail]
711
+ end
712
+
713
+ @headers["content-type"] = hdr
714
+ end
715
+
716
+
717
+ #
718
+ # render
719
+ #
720
+ def render filename, context = binding
721
+ filename = File.join( @path_prefix, filename )
722
+ unless File.exist?(filename) and !File.directory?(filename)
723
+ raise TemplateNotFound
724
+ end
725
+ template = File.read( filename )
726
+ ERB.new( template ).result( context )
727
+ end
728
+
729
+ #
730
+ # redirect_to
731
+ # Sets up a 301 redirect and returns a empty string.
732
+ # So it can be used as the last line in a controller.
733
+ #
734
+ def redirect_to url
735
+ @headers['Location'] = url
736
+ @http_response_code = 301
737
+ ""
738
+ end
739
+
740
+
741
+ end
742
+
743
+
744
+ end # module Monorail
745
+
746
+
747
+
748
+ #-----------------------------------------------------
749
+
750
+ if __FILE__ == $0
751
+ puts "No default behavior for #{__FILE__}"
752
+ end
753
+