root/HTTP/HTTPExtra.lua

Revision 1461 (checked in by rsz, 2 weeks ago)

cleanup

Line 
1 --------------------------------------------------------------------------------
2 -- Title:               HTTPExtra.lua
3 -- Description:         Like a square peg in a round hole
4 -- Author:              Raphaël Szwarc http://alt.textdrive.com/lua/
5 -- Creation Date:       January 30, 2007
6 -- Legal:               Copyright (C) 2007 Raphaël Szwarc
7 --                      Under the terms of the MIT License
8 --                      http://www.opensource.org/licenses/mit-license.html
9 --------------------------------------------------------------------------------
10
11 -- import dependencies
12 local HTTP = require( 'HTTP' )
13
14 local io = require( 'io' )
15 local os = require( 'os' )
16 local table = require( 'table' )
17
18 local getfenv = getfenv
19 local getmetatable = getmetatable
20 local ipairs = ipairs
21 local module = module
22 local package = package
23 local pairs = pairs
24 local pcall = pcall
25 local rawget = rawget
26 local require = require
27 local setmetatable = setmetatable
28 local tonumber = tonumber
29 local tostring = tostring
30 local type = type
31 local unpack = unpack
32
33 --------------------------------------------------------------------------------
34 -- HTTPExtra
35 --------------------------------------------------------------------------------
36
37 module( 'HTTPExtra' )
38
39 --------------------------------------------------------------------------------
40 -- HTTPRequest (Extra)
41 --------------------------------------------------------------------------------
42
43 module( 'HTTPRequest' )
44
45 local self = _M
46
47 local function Forward( aValue )
48     if aValue and aValue:len() > 0 then
49         aValue = aValue:match( '^([^,]+)' )
50        
51         if aValue and aValue:len() > 0 then
52             return aValue
53         end
54     end
55 end
56
57 function AddressFilter( aRequest, aResponse )
58     if not aRequest.address then
59         local anAddress = os.getenv( 'TCPREMOTEIP' )
60         local anEnvironment = getfenv() or {}
61         local isForwarded = anEnvironment[ 'forwarded' ]
62        
63         if isForwarded then
64             local aForward = Forward( aRequest.header[ 'x-forwarded-for' ] )
65
66             anAddress = aForward or anAddress
67         end
68        
69         if anAddress then
70             -- Requires Diego Nehab's LuaSocket library
71             -- http://www.tecgraf.puc-rio.br/~diego/professional/luasocket/home.html
72             local ok, socket = pcall( require, 'socket' )
73            
74             if ok and socket then
75                 anAddress = socket.dns.toip( anAddress ) or anAddress
76             end
77         end
78        
79         aRequest.address = anAddress
80     end
81 end
82
83 function HostFilter( aRequest, aResponse )
84     if not aRequest.host then
85         -- Requires Diego Nehab's LuaSocket library
86         -- http://www.tecgraf.puc-rio.br/~diego/professional/luasocket/home.html
87         local ok, socket = pcall( require, 'socket' )
88         local anAddress = aRequest.address
89         local aHost = os.getenv( 'TCPREMOTEHOST' )
90        
91         if ok and socket and anAddress then
92             aHost = socket.dns.tohostname( anAddress ) or anAddress
93         end
94
95         aRequest.host = aHost
96     end
97 end
98
99 function AuthorizationFilter( aRequest, aResponse )
100     if not aRequest.authorization and not aResponse.authorization then
101         local aHeader = aRequest.header[ 'authorization' ]
102    
103         aRequest.authorization = {}
104         aResponse.authorization = { scheme = 'Basic' }
105    
106         if aHeader then
107             local aScheme, aCredential = aHeader:match( '(%S+)%s(%S+)' )
108            
109             if aScheme and aCredential and aScheme:find( 'Basic' ) then
110                 local URL = require( 'URL' )
111                 local URLParameter = require( 'URLParameter' )
112                 local base64 = require( 'base64' )
113                 local aUser, aPassword = base64( aCredential ):match( '(.*):(.*)' )
114                 local aParameter = URLParameter( ( 'user=%s&password=%s' ):format( aUser, aPassword ) )
115                 local aUser = aParameter.user
116                 local aPassword = aParameter.password
117                 local anAuthorization = { scheme = aScheme, user = aUser, password = aPassword }
118
119                 aRequest.authorization = anAuthorization
120             end
121         end
122     end
123 end
124
125 function CookieFilter( aRequest, aResponse )
126     if not aRequest.cookie and aResponse.cookie then
127         local HTTPCookie = require( 'HTTPCookie' )
128         local aHeader = aRequest.header[ 'cookie' ]
129         local aCookie = HTTPCookie( aHeader )
130        
131         aRequest.cookie = aCookie
132         aResponse.cookie = HTTPCookie( aCookie )
133     end
134 end
135
136 self.filter[ #self.filter + 1 ] = AddressFilter
137 self.filter[ #self.filter + 1 ] = HostFilter
138 self.filter[ #self.filter + 1 ] = AuthorizationFilter
139 self.filter[ #self.filter + 1 ] = CookieFilter
140
141 --------------------------------------------------------------------------------
142 -- HTTPResponse (Extra)
143 --------------------------------------------------------------------------------
144
145 module( 'HTTPResponse' )
146
147 local self = _M
148
149 function AuthorizationFilter( aRequest, aResponse )
150     if not aResponse.header[ 'www-authenticate' ] then
151         local anAuthorization = aResponse.authorization or {}
152         local aScheme = anAuthorization.scheme
153         local aRealm = anAuthorization.realm
154        
155         if aScheme == 'Basic' and aRealm then
156             aResponse.status.code = 401
157             aResponse.status.description = 'Unauthorised'
158             aResponse.header[ 'www-authenticate' ] = ( '%s realm="%s"' ):format( aScheme, aRealm )
159            
160             if not aResponse.content then
161                 aResponse.header[ 'content-type' ] = 'text/plain'
162                 aResponse.content = '401 Unauthorised'
163             end
164         end
165     end
166 end
167
168 function CookieFilter( aRequest, aResponse )
169     if aResponse.cookie and not aResponse.header[ 'set-cookie' ] then
170         aResponse.header[ 'set-cookie' ] = tostring( aResponse.cookie )
171     end
172 end
173
174 function GZIPFilter( aRequest, aResponse )
175     local aCode = aResponse.status.code
176     local anEncoding = ( aResponse.header[ 'content-encoding' ] or '' ):lower()
177    
178     if aCode >= 200 and aCode <= 299 and not anEncoding:find( 'gzip', 1, true ) then
179         local aLength = tonumber( aResponse.header[ 'content-length' ] ) or 0
180         local anEncoding = ( aRequest.header[ 'accept-encoding' ] or '' ):lower()
181        
182         if aLength > 0 and anEncoding:find( 'gzip', 1, true ) then
183             -- Requires Tiago Dionizio's lzlib library
184             -- http://luaforge.net/projects/lzlib/
185             local ok, zlib = pcall( require, 'zlib' )
186            
187             if ok and zlib then
188                 local aContent = tostring( aResponse.content )
189                 local zContent = zlib.compress( aContent, 9, nil, 15 + 16 )
190                
191                 if zContent:len() < aContent:len() then
192                     local aVary = ( aResponse.header[ 'vary' ] or 'accept-encoding' ):lower()
193                    
194                     if aVary ~= 'accept-encoding' then
195                         aVary = aVary .. ',' .. 'accept-encoding'
196                     end
197                
198                     aResponse.header[ 'content-encoding' ] = 'gzip'
199                     aResponse.header[ 'vary' ] = aVary
200                     aResponse.content = zContent
201                 end
202             end
203         end
204     end
205 end
206
207 function ETagFilter( aRequest, aResponse )
208     local aCode = aResponse.status.code
209    
210     if aCode >= 200 and aCode <= 299 then
211         local aLength = tonumber( aResponse.header[ 'content-length' ] ) or 0
212         local anEtag = aResponse.header[ 'etag' ]
213
214         if aLength > 0 and not anEtag then
215             -- Requires Klaus Ripke's slncrypto library
216             -- http://luaforge.net/projects/sln/
217             local ok, crypto = pcall( require, 'crypto' )
218        
219             if ok and crypto then
220                 aResponse.header[ 'etag' ] = crypto.sha1( tostring( aResponse.content ) )
221             end
222         end
223     end
224 end
225
226 function RangeFilter( aRequest, aResponse )
227     local aCode = aResponse.status.code
228    
229     if aCode >= 200 and aCode <= 299 then
230         local aLength = tonumber( aResponse.header[ 'content-length' ] ) or 0
231         local aRange = ( aRequest.header[ 'range' ] or '' ):lower()
232
233         if aLength > 0 and aRange ~= '' then
234         end
235     end
236 end
237
238 function ConditionalFilter( aRequest, aResponse )
239     local aCode = aResponse.status.code
240    
241     if aCode >= 200 and aCode <= 299 then
242         local modifiedSince = aRequest.header[ 'if-modified-since' ] or 0
243         local lastModified = aResponse.header[ 'last-modified' ] or 1
244         local noneMatch = aRequest.header[ 'if-none-match' ] or 0
245         local etag = aResponse.header[ 'etag' ] or 1
246        
247         if modifiedSince == lastModified or noneMatch == etag then
248             aResponse.status.code = 304
249             aResponse.status.description = 'Not Modified'
250         end
251     end   
252 end
253
254 function ContentFilter( aRequest, aResponse )
255     local aMethod = ( aRequest.status.method or '' ):lower()
256     local aCode = aResponse.status.code or 200
257    
258     if aMethod == 'head' or ( aCode >= 100 and aCode < 200 ) or aCode == 204 or aCode == 304 then
259         aResponse.content = nil
260     end
261 end
262
263 local months = { jan = 1, feb = 2, mar = 3, apr = 4, may = 5, jun = 6, jul = 7, aug = 8, sep = 9, oct = 10, nov = 11, dec = 12 }
264
265 local function DateTime( aValue )
266     -- e.g. "Fri, 05 Jan 2007 17:27:31 GMT"
267     local aPattern = '(%d%d?) (%a%a%a) (%d%d%d%d) (%d%d):(%d%d):(%d%d)'
268     local aDay, aMonth, aYear, aHour, aMinute, aSecond = aValue:match( aPattern )
269     local aYear = tonumber( aYear )
270     local aMonth = months[ aMonth:lower() ]
271     local aDay = tonumber( aDay )
272     local aHour = tonumber( aHour )
273     local aMinute = tonumber( aMinute )
274     local aSecond = tonumber( aSecond )
275     local aDate = { year = aYear, month = aMonth, day = aDay, hour = aHour, min = aMinute, sec = aSecond }
276     local aTime = os.time( aDate )
277    
278     return aTime
279 end
280
281 function ExpiresFilter( aRequest, aResponse )
282     local lastModified = aResponse.header[ 'last-modified' ]
283     local aDate = aResponse.header[ 'date' ]
284     local anExpires = aResponse.header[ 'expires' ]
285    
286     if lastModified and aDate and not anExpires then
287         local lastModified = DateTime( lastModified )
288         local aDate = DateTime( aDate )
289         local anInterval = aDate - lastModified
290        
291         if anInterval > 0 then
292             aResponse.header[ 'expires' ] = os.date( '!%a, %d %b %Y %H:%M:%S GMT', aDate + ( anInterval / 2 ) )
293         end
294     end
295 end
296
297 function LogFilter( aRequest, aResponse )
298     local aWriter = io.stderr
299     local aHost = aRequest.host or '-'
300     local anAuthorization = aRequest.authorization or {}
301     local aUser = anAuthorization.user or '-'
302     local aDate = ( '[%s]' ):format( os.date( '!%d/%b/%Y:%H:%M:%S -0000', os.time() ) )
303     local aLine = ( '"%s %s %s"' ):format( aRequest.status.method, aRequest.status.uri, aRequest.status.version )
304     local aStatus = aResponse.status.code or '-'
305     local aLength = aResponse.header[ 'content-length' ] or '-'
306     local aReferer = aRequest.header[ 'referer' ] or '-'
307     local anAgent = aRequest.header[ 'user-agent' ] or '-'
308
309     if aReferer ~= '-' then
310         aReferer = ( '%q' ):format( aReferer )
311     end
312
313     if anAgent ~= '-' then
314         anAgent = ( '%q' ):format( anAgent )
315     end
316
317     aWriter:write( aHost, ' ' )
318     aWriter:write( '-', ' ' )
319     aWriter:write( aUser, ' ' )
320     aWriter:write( aDate, ' ' )
321     aWriter:write( aLine, ' ' )
322     aWriter:write( aStatus, ' ' )
323     aWriter:write( aLength, ' ' )
324     aWriter:write( aReferer, ' ' )
325     aWriter:write( anAgent, '\n' )
326     aWriter:flush()
327 end
328
329 self.filter[ #self.filter + 1 ] = AuthorizationFilter
330 self.filter[ #self.filter + 1 ] = CookieFilter
331 self.filter[ #self.filter + 1 ] = GZIPFilter
332 self.filter[ #self.filter + 1 ] = ETagFilter
333 self.filter[ #self.filter + 1 ] = RangeFilter
334 self.filter[ #self.filter + 1 ] = ConditionalFilter
335 self.filter[ #self.filter + 1 ] = ContentFilter
336 self.filter[ #self.filter + 1 ] = ExpiresFilter
337 self.filter[ #self.filter + 1 ] = LogFilter
338
339 --------------------------------------------------------------------------------
340 -- HTTPCookie
341 -- as per Xavante's Cookies module
342 -- http://www.keplerproject.org/xavante/
343 --------------------------------------------------------------------------------
344
345 module( 'HTTPCookie' )
346 _VERSION = '1.0'
347
348 local self = setmetatable( _M, {} )
349 local meta = getmetatable( self )
350
351 local function ReadCookie( aValue )
352     local someCookies = {}
353     local aName = nil
354    
355     for aKey, aValue in ( aValue or '' ):gmatch( '([^%s;=]+)%s*=%s*"([^"]*)"' ) do
356         aKey = aKey:lower()
357    
358         if aKey:byte() == 36 then -- $option
359             if aName then
360                 local anOption = aKey:sub( 2 )
361                    
362                 someCookies[ aName ].option[ anOption ] = aValue
363             end
364         else
365             someCookies[ aKey ] = { value = aValue, option = {} }
366             aName = aKey
367         end
368     end
369    
370     return someCookies
371 end
372
373 local function WriteCookie( aValue )
374     local aBuffer = {}
375        
376     for aName, aCookie in pairs( aValue or {} ) do
377         local aFormat = ( '%s="%s";version="1"' ):format( aName, aCookie.value )
378         local anOption = aCookie.option
379        
380         if anOption then
381             for aKey, aValue in pairs( anOption ) do
382                 aFormat = aFormat .. ( ';%s="%s"' ):format( aKey:lower(), tostring( aValue ) )
383             end
384         end
385                        
386         aBuffer[ #aBuffer + 1 ] = aFormat
387     end
388        
389     if #aBuffer > 0 then
390         return table.concat( aBuffer, '' )
391     end
392    
393     return nil
394 end
395
396 local function NewCookie( aValue )
397     local aCookie = nil
398    
399     if type( aValue ) == 'table' then
400         aCookie = ReadCookie( WriteCookie( aValue ) )
401     else
402         aCookie = ReadCookie( tostring( aValue or '' ) )
403     end
404    
405     setmetatable( aCookie, self )
406    
407     return aCookie
408 end
409
410 function meta:__call( aValue )
411     return NewCookie( aValue )
412 end
413
414 function self:__concat( aValue )
415     return tostring( self ) .. tostring( aValue )
416 end
417
418 function self:__tostring()
419     return WriteCookie( self )
420 end
421
422 --------------------------------------------------------------------------------
423 -- HTTPFile
424 --------------------------------------------------------------------------------
425
426 module( 'HTTPFile' )
427 _VERSION = '1.0'
428
429 local self = setmetatable( _M, {} )
430 local meta = getmetatable( self )
431
432 local function File( aDirectory, aName )
433     if aDirectory and aName then
434         local aSeparator = package.path:match( '(%p)%?%.' ) or '/'
435         local aPath = aDirectory .. aName:gsub( '%.%.', '' ):gsub( '\\', '' ):gsub( '/', aSeparator )
436         local aFile = io.open( aPath, 'rb' )
437        
438         return aFile, aPath
439     end
440 end
441
442 local function Size( aFile )
443     local aSize = aFile:seek( 'end' )
444    
445     aFile:seek( 'set' )
446    
447     return aSize
448 end
449
450 local function Modification( aPath )
451     -- Requires Kepler's LuaFileSystem
452     -- http://www.keplerproject.org/luafilesystem/
453     local ok, lfs = pcall( require, 'lfs' )
454    
455     if ok and lfs then
456         return lfs.attributes( aPath, 'modification' )
457     end
458 end
459
460 local function Hash( aPath, aSize, aModification  )
461     if aPath and aSize and aModification then
462         -- Requires Klaus Ripke's slncrypto library
463         -- http://luaforge.net/projects/sln/
464         local ok, crypto = pcall( require, 'crypto' )
465    
466         if ok and crypto then
467             return crypto.sha1( aPath .. ':' .. aSize .. ':' .. aModification )
468         end
469     end
470 end
471
472 local function Type( aName )
473     local MIME = require( 'MIME' )
474     local MIMEType = require( 'MIMEType' )
475     local anExtension = ( aName:match( '^.+%.(%w+)$' ) or '' ):lower()
476    
477     return MIMEType[ anExtension ] or 'application/octet-stream'
478 end
479
480 local function Content( aFile, aSize, aChunk )
481     if aSize <= aChunk * 10 then
482         local aContent = aFile:read( '*a' )
483
484         aFile:close()
485        
486         return aContent
487     end
488
489     return function()
490         local aContent = aFile:read( aChunk )
491        
492         if aContent then
493             return aContent
494         end
495        
496         aFile:close()
497     end
498 end
499
500 function meta:__call( aDirectory, aChunk )
501     aDirectory = tostring( aDirectory or '' )
502     aChunk = tonumber( aChunk ) or 8192
503
504     return function( aName )
505         local aFile, aPath = File( aDirectory, aName )
506
507         if aFile then
508             local aSize = Size( aFile )
509             local aModification = Modification( aPath )
510        
511             if aModification then
512                 HTTP.response.header[ 'last-modified' ] = os.date( '!%a, %d %b %Y %H:%M:%S GMT', aModification )
513                 HTTP.response.header[ 'etag' ] = Hash( aPath, aSize, aModification )
514             end
515            
516             HTTP.response.header[ 'content-disposition' ] = ( 'inline; filename="%s"' ):format( aName )
517             HTTP.response.header[ 'content-type' ] = Type( aName )
518
519             return Content( aFile, aSize, aChunk )
520         end
521     end
522 end
523
524 --------------------------------------------------------------------------------
525 -- HTTPService
526 --------------------------------------------------------------------------------
527
528 module( 'HTTPService' )
529 _VERSION = '1.0'
530
531 local self = setmetatable( _M, {} )
532 local meta = getmetatable( self )
533
534 local function Type( anObject )
535     if type( anObject ) == 'table' then
536         return anObject._NAME or ( getmetatable( anObject ) or {} )._NAME
537     end
538    
539     return type( anObject )
540 end
541
542 local function Capitalize( aValue )
543     return ( aValue:lower():gsub( '(%l)([%w_\']*)', function( first, rest ) return first:upper() .. rest end ) )
544 end
545
546 local function Argument( anObject, anAction )
547     local someArguments = { anObject }
548     local aReader = function( aValue )
549         someArguments[ #someArguments + 1 ] = tonumber( aValue ) or aValue
550     end
551
552     anAction:gsub( '([^%.]+)', aReader )
553    
554     return someArguments
555 end
556
557 local function Handler( anObject, aMethod, anAction, isLast )
558     local aHandlerAction, anArgument = ( anAction or '' ):match( '([^%.]*)%.?(.*)' )
559    
560     if not isLast then
561         aMethod = 'get'
562     end
563    
564     aHandlerAction = aMethod:lower() .. Capitalize( aHandlerAction )
565    
566     if type( anObject ) == 'table' and type( anObject[ aHandlerAction ] ) == 'function' then
567         local someArguments = Argument( anObject, anArgument )
568        
569         return anObject[ aHandlerAction ], someArguments
570     end
571    
572     if type( anObject ) == 'string' then
573         return function() return anObject end, {}
574     end
575
576     if type( anObject ) == 'function' then
577         return function() return anObject end, {}
578     end
579
580     if aMethod:lower() == 'head' then
581         return Handler( anObject, 'get', anAction, isLast )
582     end
583    
584     return function() end, {}
585 end
586
587 local function Action()
588     for aKey, aValue in pairs( HTTP.request.parameter ) do
589         local aParameter = aKey:match( '^action%.([%w%p]+)$' )
590        
591         if aParameter then
592             return aParameter
593         end
594     end
595 end
596
597 local function Iterator( aURL, aBase )
598     local URL = require( 'URL' )
599     local URLPath = require( 'URLPath' )
600     local aPath = URLPath( tostring( aURL.path ):sub( tostring( aBase.path ):len() + 1 ) )
601     local aServicePath = URLPath( aBase.path )
602     local anAction = Action()
603     local aCount = #aPath + 1
604     local anIndex = 1
605    
606     if anAction then
607         aPath[ #aPath + 1 ] = anAction
608         aCount = #aPath
609     else
610         aPath[ #aPath + 1 ] = ''
611         aCount = #aPath
612     end
613    
614     return function()
615         if anIndex <= aCount then
616             local isLast = anIndex == aCount
617             local aComponent = aPath[ anIndex ]
618            
619             if anIndex > 1 then
620                 aServicePath = aServicePath( aPath[ anIndex - 1 ] )
621             end
622
623             anIndex = anIndex + 1
624
625             return URLPath( aServicePath ), aComponent, isLast
626         end
627     end
628 end
629
630 local function Dispatch( aService, aURL, anObject, aMethod )
631     local aBase = aService[ anObject ]
632     local aLocation = nil
633
634     if not aBase then
635         return nil
636     end
637    
638     if not tostring( aURL.path ):find( tostring( aBase.path ), 1, true ) then
639         return nil, aBase
640     end
641
642     for aPath, anAction, isLast in Iterator( aURL, aBase ) do
643         local aHandler, someArguments = Handler( anObject, aMethod, anAction, isLast )
644        
645         if type( anObject ) == 'table' then
646             anObject.path = aPath
647         end
648
649         anObject, aLocation = aHandler( unpack( someArguments ) )
650
651         if aLocation then
652             return anObject, aLocation
653         end
654     end
655            
656     return anObject, aLocation
657 end
658
659 function meta:__call( aPrefix, anObject, toURL, toObject )
660     local aPattern = aPrefix  .. '.*'
661     local toURL = toURL or anObject[ 'toURL' ]
662     local toObject = toObject or anObject[ 'toObject' ]
663     local aType = Type( anObject )
664     local aService = { prefix = aPrefix, pattern = aPattern, type = aType, toURL = toURL, toObject = toObject }
665    
666     self[ aType ] = aService
667    
668     setmetatable( aService, self )
669    
670     return aService
671 end
672
673 function meta:__index( aKey )
674     local aService = rawget( self, Type( aKey ) )
675    
676     if aService then
677         return aService[ aKey ]
678     end
679 end
680
681 function self:__call()
682     return function()
683         local aRequest = HTTP.request
684         local aURL = aRequest.url
685         local anObject = self[ aURL ]
686         local aMethod = aRequest.status.method
687        
688         return Dispatch( self, aURL, anObject, aMethod )
689     end
690 end
691
692 function self:__index( aKey )
693     if Type( aKey ) == self.type then
694         return HTTP.request.url + self:toURL( aKey )
695     elseif Type( aKey ) == 'URL' then
696         aKey = require( 'URL' )( tostring( aKey ):gsub( '(%.[^/]*)', '' ) )
697    
698         return self:toObject( aKey )
699     end
700 end
701
702 function self:__eq( aValue )
703     return tostring( self ) == tostring( aValue )
704 end
705
706 function self:__lt( aValue )
707     return tostring( self ) < tostring( aValue )
708 end
709
710 function self:__tostring()
711     return self.prefix
712 end
713
714
Note: See TracBrowser for help on using the browser.