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.
- data/README +37 -0
- data/RELEASE_NOTES +6 -0
- data/bin/monorail +8 -0
- data/ext/extconf.rb +33 -0
- data/ext/http.cpp +419 -0
- data/ext/http.h +154 -0
- data/ext/rubyhttp.cpp +165 -0
- data/lib/monorail.rb +137 -0
- data/lib/monorail/app.rb +132 -0
- data/lib/monorail/monorail_webd.rb +753 -0
- data/lib/monorail/monotreme.rb +40 -0
- data/tests/testem.rb +4 -0
- metadata +64 -0
data/lib/monorail/app.rb
ADDED
@@ -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
|
+
|