root/LW/CGI.lua

Revision 956 (checked in by rsz, 2 years ago)

cleanup

Line 
1 --------------------------------------------------------------------------------
2 -- Title:               CGI.lua
3 -- Description:         Like a square peg in a round hole
4 -- Author:              Raphaël Szwarc http://alt.textdrive.com/lua/
5 -- Creation Date:       February 1, 2006
6 -- Legal:               Copyright (C) 2006 Raphaël Szwarc
7 --------------------------------------------------------------------------------
8
9 -- import dependencies
10 local debug = require( "debug" )
11 local io = require( "io" )
12 local os = require( "os" )
13 local string = require( "string" )
14 local table = require( "table" )
15
16 --------------------------------------------------------------------------------
17 -- Utility functions
18 --------------------------------------------------------------------------------
19
20 local function Copy( aValue )
21         if type( aValue ) == "table" then
22                 local aCopy = {}
23                
24                 for aKey, aValue in pairs( aValue ) do
25                         aCopy[ Copy( aKey ) ] = Copy( aValue )
26                 end
27        
28                 aValue = aCopy
29         end
30
31         return aValue
32 end
33
34 local function Log( ... )
35         local aLength = select( "#", ... )
36         local aBuffer = {}
37        
38         for anIndex = 1, aLength do
39                 aBuffer[ #aBuffer + 1 ] = tostring( select( anIndex, ... ) )
40         end
41
42         io.stderr:write( os.date( "%m/%d %H:%M:%S", os.time() ), " ", table.concat( aBuffer, "\t" ), "\n" )
43 end
44
45 local encodings = { [ "&" ] = "&amp;", [ "<" ] = "&lt;", [ ">" ] = "&gt;", [ "\"" ] = "&quot;", [ "'" ] = "&apos;" }
46
47 function string:encode()
48         return ( self:gsub( "%W", encodings ) )
49 end
50
51 --------------------------------------------------------------------------------
52 -- Meta methods
53 --------------------------------------------------------------------------------
54
55 local Meta = {}
56
57 function Meta.__concat( anObject, anotherObject )
58         return tostring( anObject ) .. tostring( anotherObject )
59 end
60
61 function Meta.__tostring( anObject )
62         return anObject:toString()       
63 end
64
65 --------------------------------------------------------------------------------
66 -- URL methods
67 --------------------------------------------------------------------------------
68
69 local URL = {}
70
71 setmetatable( URL, Meta )
72
73 function URL:host()
74         return ( os.getenv( "SERVER_NAME" ) or "localhost" ):lower()
75 end
76
77 function URL:path()
78         local aPath = os.getenv( "SCRIPT_NAME" ) or ""
79        
80         aPath = aPath .. ( os.getenv( "PATH_INFO" ) or "/" )
81        
82         return self:decode( aPath )
83 end
84
85 function URL:port()
86         return tonumber( os.getenv( "SERVER_PORT" ) ) or 80
87 end
88
89 function URL:query()
90         return os.getenv( "QUERY_STRING" ) or ""
91 end
92
93 function URL:decode( aValue )
94         local aFunction = function( aValue )
95                 return string.char( tonumber( aValue, 16 ) )
96         end
97        
98         aValue = aValue:gsub( "%%(%x%x)", aFunction )
99        
100         return aValue
101 end
102
103 function URL:decodeParameters( aValue )
104         local someParameters = {}
105        
106         for aKey, aValue in aValue:gmatch( "([^&=]+)=([^&=]+)" ) do
107                 aKey = aKey:gsub( "+", " " )
108                 aKey = self:decode( aKey ):lower()
109                 aValue = aValue:gsub( "+", " " )
110                 aValue = self:decode( aValue )
111                
112                 someParameters[ aKey ] = aValue
113         end
114        
115         return someParameters
116 end
117
118 function URL:encode( aValue )
119         local aFunction = function( aValue )
120                 return ( "%%%02X" ):format( aValue:byte() )
121         end
122        
123         return ( aValue:gsub( "([^A-Za-z0-9_%-%.])", aFunction ) )
124 end
125
126 function URL:encodeParameters( someValues )
127         local aBuffer = {}
128        
129         for aKey, aValue in pairs( someValues ) do
130                 aKey = tostring( aKey ):gsub( " ", "+" )
131                 aBuffer[ #aBuffer + 1 ] = self:encode( aKey ):lower()
132                 aBuffer[ #aBuffer + 1 ] = "="
133                 aValue = tostring( aValue ):gsub( " ", "+" )
134                 aBuffer[ #aBuffer + 1 ] = self:encode( aValue  )
135         end
136        
137         return table.concat( aBuffer, "" )
138 end
139
140 function URL:queries()
141         return self:decodeParameters( self:query() )
142 end
143
144 function URL:scheme()
145         return ( os.getenv( "SERVER_PROTOCOL" ) or "http" ):match( "(%a+)" ):lower()
146 end
147
148 function URL:toString()
149         local aBuffer = {}
150        
151         aBuffer[ #aBuffer + 1 ] = self:scheme()
152         aBuffer[ #aBuffer + 1 ] = "://"
153         aBuffer[ #aBuffer + 1 ] = self:host()
154        
155         if self:port() ~= 80 then
156                 aBuffer[ #aBuffer + 1 ] = ":"
157                 aBuffer[ #aBuffer + 1 ] = tostring( self:port() )
158         end       
159        
160         aBuffer[ #aBuffer + 1 ] = self:path()
161        
162         if self:query() and self:query():len() > 0 then
163                 aBuffer[ #aBuffer + 1 ] = "?"
164                 aBuffer[ #aBuffer + 1 ] = self:query()
165         end
166        
167         return table.concat( aBuffer, "" )
168 end
169
170 --------------------------------------------------------------------------------
171 -- Request methods
172 --------------------------------------------------------------------------------
173
174 local Request = {}
175
176 setmetatable( Request, Meta )
177
178 function Request:address()
179         return os.getenv( "REMOTE_ADDR" ) or ""
180 end
181
182 function Request:authentication()
183         return os.getenv( "AUTH_TYPE" ) or ""
184 end
185
186 function Request:content()
187         if not self._content then
188                 local aLength = self:contentLength()
189                 local aContent = ""
190                
191                 if aLength > 0 then
192                         aContent = self:reader():read( aLength )
193                 end
194                
195                 self._content = aContent or ""
196         end
197        
198         return self._content
199 end
200
201 function Request:contentLength()
202         return tonumber( os.getenv( "CONTENT_LENGTH" ) ) or 0
203 end
204
205 function Request:contentType()
206         return os.getenv( "CONTENT_TYPE" ) or ""
207 end
208
209 function Request:cookie( aKey )
210         local aKey = tostring( aKey ):lower()
211         local aCookie = self:cookies()[ aKey ]
212        
213         if aCookie then
214                 return aCookie.value, aCookie.options
215         end
216        
217         return nil
218 end
219
220 function Request:cookies()
221         local someCookies = {}
222         local anHeader = self:header( "cookie" )
223        
224         if anHeader then
225                 local aName = nil
226        
227                 -- as per Xavante's Cookies module
228                 -- http://www.keplerproject.org/xavante/
229                 for aKey, aValue in anHeader:gmatch( '([^%s;=]+)%s*=%s*"([^"]*)"' ) do
230                         aKey = aKey:lower()
231
232                         if aKey:byte() == 36 then       -- $option
233                                 if aName then
234                                         local anOption = aKey:sub( 2 )
235                                        
236                                         someCookies[ aName ].options[ anOption ] = aValue
237                                 end
238                         else
239                                 someCookies[ aKey ] = { value = aValue, options = {} }
240                                 aName = aKey
241                         end
242                 end
243         end
244        
245         return someCookies
246 end
247
248 function Request:header( aKey )
249         local aKey = tostring( aKey ):upper():gsub( "%-", "_" )
250         local aValue = os.getenv( aKey )
251        
252         if not aValue then
253                 aKey = self:url():scheme():upper() .. "_" .. aKey
254                
255                 aValue = os.getenv( aKey )
256         end
257        
258         return aValue
259 end
260
261 function Request:method()
262         return os.getenv( "REQUEST_METHOD" ) or "GET"
263 end
264
265 function Request:parameter( aKey )
266         local aKey = tostring( aKey ):lower()
267         local someParameters = self:parameters()
268        
269         return someParameters[ aKey ]
270 end
271
272 function Request:parameters()
273         local someParameters = self:url():queries()
274         local aType = ( self:header( "content-type" ) or "" ):lower()
275
276         if aType:find( "application/x-www-form-urlencoded", 1, true ) then
277                 someParameters = self:url():decodeParameters( self:content() )
278         end
279        
280         return someParameters
281 end
282
283 function Request:reader()
284         return io.stdin
285 end
286
287 function Request:url()
288         return URL
289 end
290
291 function Request:user()
292         return os.getenv( "REMOTE_USER" ) or ""
293 end
294
295 function Request:version()
296         return os.getenv( "SERVER_PROTOCOL" ) or "HTTP/1.1"
297 end
298
299 function Request:toString()
300         local aBuffer = {}
301        
302         aBuffer[ #aBuffer + 1 ] = self:method()
303         aBuffer[ #aBuffer + 1 ] = self:url():toString()
304         aBuffer[ #aBuffer + 1 ] = self:version()
305        
306         return table.concat( aBuffer, " " )
307 end
308
309 --------------------------------------------------------------------------------
310 -- Response methods
311 --------------------------------------------------------------------------------
312
313 local Response = {}
314
315 setmetatable( Response, Meta )
316
317 function Response:contentType()
318         return self:header( "content-type" )
319 end
320
321 function Response:setContentType( aValue )
322         self:setHeader( "content-type", aValue )
323        
324         return self
325 end
326
327 function Response:cookie( aKey )
328         local aKey = tostring( aKey ):lower()
329         local aCookie = self:cookies()[ aKey ]
330        
331         if aCookie then
332                 return aCookie.value, aCookie.options
333         end
334        
335         return nil
336 end
337
338 function Response:setCookie( aKey, aValue, someOptions )
339         local aKey = tostring( aKey ):lower()
340         local someCookies = self:cookies()
341        
342         if not aValue then
343                 aValue = tostring( nil )
344                 someOptions = { [ "max-age" ] = 0 }
345         end
346        
347         someCookies[ aKey ] = { value = aValue, options = someOptions }
348        
349         return self
350 end
351
352 function Response:cookies()
353         if not self._cookies then
354                 self._cookies = Copy( Request:cookies() )
355         end
356        
357         return self._cookies
358 end
359
360 function Response:cookiesHeader()
361         local aBuffer = {}
362        
363         for aName, aCookie in pairs( self:cookies() ) do
364                 local aFormat = ( '%s="%s";version="1"' ):format( aName, aCookie.value )
365                 local someOptions = aCookie.options
366                
367                 if someOptions then
368                         for aKey, aValue in pairs( someOptions ) do
369                                 aFormat = aFormat .. ( ';%s="%s"' ):format( aKey:lower(), tostring( aValue ) )
370                         end
371                 end
372                                
373                 aBuffer[ #aBuffer + 1 ] = aFormat
374         end
375        
376         if #aBuffer > 0 then
377                 return table.concat( aBuffer, "" )
378         end
379        
380         return nil
381 end
382
383 function Response:encoding()
384         return "utf-8"
385 end
386
387 function Response:header( aKey )
388         local aKey = tostring( aKey ):lower()
389         local someHeaders = self:headers()
390
391         return someHeaders[ aKey ]
392 end
393
394 function Response:setHeader( aKey, aValue )
395         local aKey = tostring( aKey ):lower()
396         local someHeaders = self:headers()
397        
398         someHeaders[ aKey ] = aValue
399
400         return self
401 end
402
403 function Response:headers()
404         if not self._headers then
405                 local someHeaders = {}
406                
407                 someHeaders[ "content-type" ] = "text/html; charset=" .. self:encoding()
408                 someHeaders[ "date" ] =  os.date( "!%a, %d %b %Y %H:%M:%S GMT", os.time() )
409                 someHeaders[ "server" ] =  self:server()
410
411                 self._headers = someHeaders
412         end
413        
414         return self._headers
415 end
416
417 function Response:status()
418         if not self._status then
419                 self._status = 200
420                 self._statusDescription = "OK"
421         end
422        
423         return self._status, self._statusDescription
424 end
425
426 function Response:setStatus( aValue, aDescription )
427         self._status = tonumber( aValue )
428         self._statusDescription = tostring( aDescription or aValue )
429        
430         return self
431 end
432
433 function Response:server()
434         return os.getenv( "SERVER_SOFTWARE" ) or "CGI.lua"
435 end
436
437 function Response:writeHeaders()
438         if not self._writeHeaders then
439                 local aWriter = self:writer()
440                 local aSeparator = "\r\n"
441                 local aStatus, aDescription = self:status()
442                 local someHeaders = self:headers()
443                 local aCookieHeader = self:cookiesHeader()
444                
445                 aWriter:write( "status: ", aStatus, " ", aDescription, aSeparator )
446                
447                 for aKey, aValue in pairs( someHeaders ) do
448                         aWriter:write( aKey, ": ", tostring( aValue ), aSeparator )
449                 end
450                
451                 if aCookieHeader then
452                         aWriter:write( "set-cookie", ": ", aCookieHeader, aSeparator )
453                 end
454        
455                 aWriter:write( aSeparator )
456
457                 self._writeHeaders = true
458         end
459        
460         return self
461 end
462
463 function Response:write( ... )
464         local aWriter = self:writer()
465         local aLength = select( "#", ... )
466        
467         self:writeHeaders()
468
469         for anIndex = 1, aLength do
470                 local aValue = select( anIndex, ... )
471                
472                 aWriter:write( tostring( aValue ) )
473         end
474        
475         aWriter:flush()
476        
477         return self
478 end
479
480 function Response:writer()
481         return io.stdout
482 end
483
484 function Response:toString()
485         local aBuffer = {}
486         local aStatus, aDescription = self:status()
487        
488         aBuffer[ #aBuffer + 1 ] = aStatus
489         aBuffer[ #aBuffer + 1 ] = aDescription
490        
491         return table.concat( aBuffer, " " )
492 end
493
494 --------------------------------------------------------------------------------
495 -- CGI methods
496 --------------------------------------------------------------------------------
497
498 local CGI = { _DESCRIPTION = "CGI.lua", _VERSION = "1.1" }
499
500 setmetatable( CGI, Meta )
501
502 function CGI:name()
503         return os.getenv( "SCRIPT_NAME" ) or "CGI.lua"
504 end
505
506 function CGI:path()
507         return os.getenv( "PATH_TRANSLATED" ) or "."
508 end
509
510 function CGI:request()
511         return Request
512 end
513
514 function CGI:response()
515         return Response
516 end
517
518 function CGI:log()
519         return Log
520 end
521
522 function CGI:print()
523         if not self._print then
524                 self._print = function( ... )
525                         self:response():write( ... )
526                 end
527         end
528        
529         return self._print
530 end
531
532 function CGI:handlerWithMethod( anHandler, aMethod, someMatches )
533         if type( anHandler ) == "string" then
534                 anHandler = require( anHandler )
535         end
536        
537         if type( anHandler ) == "table" then
538                 table.insert( someMatches, 1, anHandler )
539
540                 anHandler = anHandler[ aMethod:lower() ]
541         end
542        
543         if type( anHandler ) == "function" then
544                 local anEnviromnent = { log = self:log(), print = self:print() }
545        
546                 setmetatable( anEnviromnent, { __index = _G } )
547
548                 setfenv( anHandler, anEnviromnent )
549         else
550                 error( "cannot resolve handler '" .. tostring( anHandler ) .. "' of type '" .. type( anHandler ) .. "'" )
551         end
552
553         return anHandler, someMatches
554 end
555
556 function CGI:dispatch( someMappings )
557         local aName = ( self:name() .. "/" ):gsub( "(%W)", "%%%1" )
558         local aMethod = self:request():method()
559         local aPath = self:request():url():path()
560        
561         for anIndex, aMapping in ipairs( assert( someMappings, "missing mappings" ) ) do
562                 local aPattern = assert( aMapping[ 1 ], "mappings: missing pattern at " .. anIndex )
563                 local aPattern = "^" .. aName .. aPattern .. "$"
564
565                 if aPath:find( aPattern ) then
566                         local someMatches = { aPath:match( aPattern ) }       
567                         local anHandler = assert( aMapping[ 2 ], "mappings: missing handler at " .. anIndex )
568                         local anHandler, someMatches = self:handlerWithMethod( anHandler, aMethod, someMatches )
569                        
570                         anHandler( unpack( someMatches ) )
571                        
572                         return self
573                 end
574         end
575        
576         self:response():setContentType( "text/plain" )
577         self:response():setStatus( 404, "Not Found" )
578         self:response():write( "404 Not Found" )
579
580         return self
581 end
582
583 function CGI:run( someMappings )
584         local aFunction = function() return self:dispatch( someMappings ) end
585         local aStatus, anException = xpcall( aFunction, debug.traceback )
586        
587         if not aStatus then
588                 local aContent = "500 Internal Server Error\r\n"
589                
590                 aContent = aContent .. "\r\n" .. tostring( anException ) .. "\r\n"
591        
592                 self:response():setContentType( "text/plain" )
593                 self:response():setStatus( 500, "Internal Server Error" )
594                 self:response():write( aContent  )
595                
596                 return nil, anException
597         end
598        
599         return self
600 end
601
602 function CGI:version()
603         return os.getenv( "GATEWAY_INTERFACE" ) or "CGI/1.1"
604 end
605
606 function CGI:toString()
607         return self:name()
608 end
609
610 return CGI
Note: See TracBrowser for help on using the browser.