monorail 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+