root/HTTP/HTTP.lua

Revision 1558 (checked in by rsz, 5 months ago)

cleanup

Line 
1 --------------------------------------------------------------------------------
2 -- Title:               HTTP.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 debug = require( 'debug' )
13 local io = require( 'io' )
14 local os = require( 'os' )
15 local table = require( 'table' )
16
17 local collectgarbage = collectgarbage
18 local getmetatable = getmetatable
19 local ipairs = ipairs
20 local module = module
21 local pairs = pairs
22 local rawset = rawset
23 local require = require
24 local select = select
25 local setmetatable = setmetatable
26 local tonumber = tonumber
27 local tostring = tostring
28 local type = type
29 local unpack = unpack
30 local xpcall = xpcall
31
32 --------------------------------------------------------------------------------
33 -- Utilities
34 --------------------------------------------------------------------------------
35
36 local function Capitalize( aValue )
37     return ( aValue:lower():gsub( '(%l)([%w_\']*)', function( first, second ) return first:upper() .. second end ) )
38 end
39
40 local function Trim( aValue )
41     return ( aValue:gsub( '^[%c%s]+', '' ):gsub( '[%s%c]+$', '' ) )
42 end
43
44 local function Sort( aMap )
45     local aList = {}
46     local anIndex = 1
47    
48     for aKey, aValue in pairs( aMap ) do
49         aList[ #aList + 1 ] = aKey
50     end
51    
52     table.sort( aList )
53    
54     return function()
55         local aKey = aList[ anIndex ]
56         local aValue = aMap[ aKey ]
57        
58         anIndex = anIndex + 1
59        
60         return aKey, aValue
61     end
62 end
63
64 --------------------------------------------------------------------------------
65 -- HTTPRequest
66 --------------------------------------------------------------------------------
67
68 module( 'HTTPRequest' )
69 _VERSION = '1.0'
70
71 local self = setmetatable( _M, {} )
72 local meta = getmetatable( self )
73
74 function RequestFilter( aRequest, aResponse )
75     local aStatus = aRequest.status
76    
77     if not aStatus
78     or not aStatus.method
79     or not aStatus.uri
80     or not aStatus.version
81     or aStatus.method ~= aStatus.method:upper()
82     or not aStatus.version:find( '^HTTP/%d%.%d$' )
83     or not aRequest.header[ 'host' ] then
84         aResponse.status.code = 400
85         aResponse.status.description = 'Bad Request'
86         aResponse.content = ( '%d %s' ):format( aResponse.status.code, aResponse.status.description )
87         aResponse.header[ 'connection' ] = 'close'
88         aResponse.header[ 'content-length' ] = aResponse.content:len()
89         aResponse.header[ 'content-type' ] = 'text/plain'
90    
91         return false
92     end
93 end
94
95 self.filter = { RequestFilter }
96
97 local function ReadStatus( aReader )
98     local aLine = Trim( aReader:read() or '' )
99     local aMethod, aURI, aVersion = aLine:match( '(%S+)%s(%S+)%s(%S+)' )
100     local aStatus = { method = aMethod, uri = aURI, version = aVersion }
101    
102     return aStatus
103 end
104
105 local function ReadHeader( aReader )
106     local aHeader = {}
107    
108     for aLine in aReader:lines() do
109         local aKey, aValue = Trim( aLine ):match( '(%S-): (.*)' )
110    
111         if aKey then
112             local aKey = Trim( aKey ):lower()
113             local aPreviousValue = aHeader[ aKey ]
114            
115             aValue = Trim( aValue )
116            
117             if aPreviousValue then
118                 aValue = aPreviousValue .. ',' .. aValue
119             end
120    
121             aHeader[ aKey ] = aValue
122         else
123             break
124         end
125     end
126    
127     return aHeader
128 end
129
130 local function ReadChunkedContent( aReader )
131     local aBuffer = {}
132     local aLine = aReader:read()
133     local aSize = tonumber( aLine, 16 )
134    
135     while aSize and aSize > 0 do
136         aBuffer[ #aBuffer + 1 ] = aReader:read( aSize )
137         aReader:read()
138    
139         aLine = aReader:read()
140         aSize = tonumber( aLine, 16 )
141     end
142    
143     aLine = aReader:read()
144    
145     while aLine and aLine ~= '' do
146         aLine = aReader:read()
147     end
148    
149     return table.concat( aBuffer )
150 end
151
152 local function ReadContent( aReader, aHeader )
153     local anEncoding = aHeader[ 'transfer-encoding' ] or ''
154     local aLength = tonumber( aHeader[ 'content-length' ] ) or 0
155    
156     if anEncoding:find( 'chunked', 1, true ) then
157         return ReadChunkedContent( aReader )
158     end
159    
160     if aLength > 0 then
161         return aReader:read( aLength )
162     end
163    
164     return nil
165 end
166
167 local function GetURL( aRequest )
168     local URL = require( 'URL' )
169     local aURL = URL( aRequest.status.uri or '/' )
170
171     aURL.path.absolute = true
172    
173     if not aURL.host and aRequest.header[ 'host' ] then
174         aURL.host = aRequest.header[ 'host' ]
175         aURL = URL( aURL )
176     end
177
178     if not aURL.scheme then
179         local HTTPRequest = require( 'HTTPRequest' )
180         local isSecure = HTTPRequest.secure
181        
182         aURL.scheme = 'http'
183        
184         if isSecure or tostring( aURL.port ):find( '443$' ) then
185             aURL.scheme = aURL.scheme .. 's'
186         end
187     end
188    
189     return aURL
190 end
191
192 local function GetParameter( aRequest )
193     local aParameter = {}
194     local aType = aRequest.header[ 'content-type' ] or ''
195    
196     for aKey, aValue in pairs( aRequest.url.query ) do
197         aParameter[ aKey ] = aValue
198     end
199    
200     if aType:lower():find( 'multipart/form-data', 1, true ) then
201         local MIME = require( 'MIME' )
202         local aContent = 'content-type: ' .. aType .. '\r\n\r\n' .. ( aRequest.content or '' )
203         local aMIME = MIME( aContent )
204        
205         for anIndex, aValue in ipairs( aMIME.content ) do
206             aParameter[ aValue.key ] = aValue.value
207             aParameter[ aValue.key .. '.filename' ] = aValue.filename
208         end
209     elseif aType:lower():find( 'application/x-www-form-urlencoded', 1, true ) then
210         local URLParameter = require( 'URLParameter' )
211         local aFormParameter = URLParameter( aRequest.content )
212
213         for aKey, aValue in pairs( aFormParameter ) do
214             aParameter[ aKey ] = aValue
215         end
216     end
217
218     return aParameter
219 end
220
221 local function NewRequest( aReader )
222     local aStatus = ReadStatus( aReader )
223     local aHeader = ReadHeader( aReader )
224     local aContent = ReadContent( aReader, aHeader )
225     local aRequest = { status = aStatus, header = aHeader, content = aContent }
226    
227     aRequest.url = GetURL( aRequest )
228     aRequest.parameter = GetParameter( aRequest )
229    
230     setmetatable( aRequest, self )
231    
232     return aRequest
233 end
234
235 local function WriteRequest( aRequest )
236     local aBuffer = {}
237     local aStatus = aRequest.status or {}
238     local aMethod = tostring( aStatus.method or '' )
239     local aURI = tostring( aStatus.uri or '' )
240     local aVersion = tostring( aStatus.version or '' )
241     local aHeader = aRequest.header or {}
242     local aContent = aRequest.content or ''
243    
244     aBuffer[ #aBuffer + 1 ] = ( '%s %s %s' ):format( aMethod, aURI, aVersion )
245    
246     for aKey, aValue in Sort( aHeader ) do
247         aBuffer[ #aBuffer + 1 ] = ( '%s: %s' ):format( Capitalize( tostring( aKey ) ), tostring( aValue ) )
248     end
249    
250     aBuffer[ #aBuffer + 1 ] = ''
251     aBuffer[ #aBuffer + 1 ] = tostring( aContent )
252    
253     return table.concat( aBuffer, '\r\n' )
254 end
255
256 function meta:__call( aReader )
257     return NewRequest( aReader )
258 end
259
260 function self:__concat( aValue )
261     return tostring( self ) .. tostring( aValue )
262 end
263
264 function self:__tostring()
265     return WriteRequest( self )
266 end
267
268 --------------------------------------------------------------------------------
269 -- HTTPResponse
270 --------------------------------------------------------------------------------
271
272 module( 'HTTPResponse' )
273 _VERSION = '1.0'
274
275 local self = setmetatable( _M, {} )
276 local meta = getmetatable( self )
277
278 function ConnectionFilter( aRequest, aResponse )
279     if not aResponse.header[ 'connection' ] then
280         local aConnection = aRequest.header[ 'connection' ]
281        
282         if aConnection then
283             aConnection = aConnection:lower()
284    
285             if aConnection:find( 'close', 1, true ) then
286                 aResponse.header[ 'connection' ] = 'close'
287             end
288            
289             if aConnection:find( 'keep-alive', 1, true ) then
290                 aResponse.header[ 'connection' ] = 'keep-alive'
291             end
292         elseif aRequest.status.version == aResponse.status.version then
293             aResponse.header[ 'connection' ] = 'keep-alive'
294         end
295     end
296 end
297
298 self.filter = { ConnectionFilter }
299
300 local function NewResponse()
301     local aStatus = { version = 'HTTP/1.1', code = 200, description = 'OK' }
302     local aHeader = { date = os.date( '!%a, %d %b %Y %H:%M:%S GMT', os.time() ) }
303     local aResponse = { status = aStatus, header = aHeader }
304        
305     setmetatable( aResponse, self )
306    
307     return aResponse
308 end
309
310 local function WriteResponse( aResponse, aWriter )
311     local aStatus = aResponse.status or {}
312     local aVersion = tostring( aStatus.version or 'HTTP/1.1' )
313     local aCode = tostring( aStatus.code or 200 )
314     local aDescription = tostring( aStatus.description or 'OK' )
315     local aHeader = aResponse.header or {}
316     local aContent = aResponse.content
317    
318     aWriter( ( '%s %s %s ' ):format( aVersion, aCode, aDescription ), '\r\n' )
319    
320     for aKey, aValue in Sort( aHeader ) do
321         aWriter( ( '%s: %s' ):format( Capitalize( tostring( aKey ) ), tostring( aValue ) ), '\r\n' )
322     end
323    
324     aWriter( '\r\n' )
325    
326     if type( aContent ) == 'function' then
327         for aValue in aContent do
328             aValue = tostring( aValue )
329             aWriter( ( '%X' ):format( aValue:len() ), '\r\n' )
330             aWriter( aValue, '\r\n' )
331         end
332         aWriter( '0\r\n\r\n' )
333     elseif aContent then
334         aWriter( tostring( aContent ) )
335     end
336 end
337
338 function meta:__call()
339     return NewResponse()
340 end
341
342 function self:__call( aWriter )
343     local aWriter = function( ... )
344         aWriter:write( ... )
345     end
346
347     WriteResponse( self, aWriter )
348    
349     return self
350 end
351
352 function self:__concat( aValue )
353     return tostring( self ) .. tostring( aValue )
354 end
355
356 function self:__tostring()
357     local aBuffer = {}
358     local aWriter = function( ... )
359         for anIndex = 1, select( '#', ... ) do
360             aBuffer[ #aBuffer + 1 ] = select( anIndex, ... )
361         end
362     end
363    
364     WriteResponse( self, aWriter )
365    
366     return table.concat( aBuffer )
367 end
368
369 --------------------------------------------------------------------------------
370 -- HTTP
371 --------------------------------------------------------------------------------
372
373 module( 'HTTP' )
374 _VERSION = '1.0'
375
376 local self = setmetatable( _M, {} )
377 local meta = getmetatable( self )
378
379 self.request = {}
380 self.response = {}
381
382 local function Filter( someFilters, aRequest, aResponse )
383     for anIndex, aFilter in ipairs( someFilters or {} ) do
384         if aFilter( aRequest, aResponse ) == false then
385             return false
386         end
387     end
388    
389     return true
390 end
391
392 local function Pattern( aMethod, aHost, aPath )
393     local aBuffer = {}
394    
395     aBuffer[ #aBuffer + 1 ] = tostring( aMethod or '.*' ):lower()
396     aBuffer[ #aBuffer + 1 ] = tostring( aHost or '.*' ):lower()
397     aBuffer[ #aBuffer + 1 ] = tostring( aPath or '.*' )
398    
399     return table.concat( aBuffer, '|' )
400 end
401
402 local function Argument( ... )
403     local someArguments = { n = select( '#', ... ) - 1, ... }
404
405     for anIndex = 1, someArguments.n do
406         local aValue = someArguments[ anIndex ]
407        
408         if aValue:len() == 0 then
409             aValue = nil
410         end
411        
412         aValue = tonumber( aValue ) or aValue
413        
414         someArguments[ anIndex ] = aValue
415     end
416    
417     return someArguments
418 end
419
420 local function Method( aHandler, aMethod, someArguments )
421     if type( aHandler ) == 'table' and aHandler[ aMethod:lower() ] then
422         someArguments = { n = someArguments.n + 1, aHandler, unpack( someArguments, 1, someArguments.n ) }
423         aHandler = aHandler[ aMethod:lower() ]
424     end
425
426     if type( aHandler ) == 'function' then
427         return aHandler, someArguments
428     end
429    
430     return function() end, {}
431 end
432
433 local function Handler( someHandlers, aRequest )
434     local aMethod = aRequest.status.method
435     local aHost = aRequest.url.host
436     local aPath = aRequest.url.path
437     local aValue = Pattern( aMethod, aHost, aPath )
438    
439     for anIndex, aList in ipairs( someHandlers ) do
440         local aPattern = '^' .. aList[ 1 ] .. '(%z?)$'
441        
442         if aValue:find( aPattern ) then
443             local aHandler = aList[ 2 ]
444             local someArguments = Argument( aValue:match( aPattern ) )
445            
446             return Method( aHandler, aMethod, someArguments )
447         end
448     end
449    
450     return function() end, {}
451 end
452
453 local function Content( someHandlers, aRequest, aResponse )
454     local aHandler, someArguments = Handler( someHandlers, aRequest )
455     local aContent, aLocation = aHandler( unpack( someArguments, 1, someArguments.n ) )
456    
457     if aContent then
458         aResponse.header[ 'content-type' ] = aResponse.header[ 'content-type' ] or 'text/html; charset=utf-8'
459         aResponse.content = aContent
460     end
461    
462     if aLocation then
463         aResponse.status.code = 302
464         aResponse.status.description = 'Found'
465         aResponse.header[ 'location' ] = aRequest.url + aLocation
466         aResponse.header[ 'content-type' ] = 'text/plain'
467         aResponse.content = aResponse.header[ 'location' ]
468     end
469    
470     if not aContent and not aLocation then
471         aResponse.status.code = 404
472         aResponse.status.description = 'Not Found'
473         aResponse.header[ 'content-type' ] = 'text/plain'
474         aResponse.content = '404 Not Found'
475     end
476 end
477
478 local function Length( aRequest, aResponse )
479     local aContent = aResponse.content
480
481     if type( aContent ) == 'function' and aRequest.status.version ~= aResponse.status.version then
482         local aBuffer = {}
483        
484         for aValue in aContent do
485             aBuffer[ #aBuffer + 1 ] = tostring( aValue )
486         end
487        
488         aResponse.content = table.concat( aBuffer )
489         aContent = aResponse.content
490     end
491
492     aResponse.header[ 'content-length' ] = nil
493
494     if type( aContent ) == 'function' then
495         aResponse.header[ 'transfer-encoding' ] = 'chunked'
496     elseif aContent then
497         aResponse.content = tostring( aContent )
498         aResponse.header[ 'content-length' ] = aResponse.content:len()
499     end
500 end
501
502 local function Dispatch( someHandlers, aRequest, aResponse )   
503     local HTTP = require( 'HTTP' )
504     local HTTPRequest = require( 'HTTPRequest' )
505     local HTTPResponse = require( 'HTTPResponse' )
506
507     HTTP.request = aRequest
508     HTTP.response = aResponse
509            
510     if Filter( HTTPRequest.filter, aRequest, aResponse ) then
511         Content( someHandlers, aRequest, aResponse )
512     end
513    
514     Length( aRequest, aResponse )
515     Filter( HTTPResponse.filter, aRequest, aResponse )
516     Length( aRequest, aResponse )
517            
518     return not ( aResponse.header[ 'connection' ] or '' ):lower():find( 'close', 1, true )
519 end
520
521 local function Call( someHandlers, aReader, aWriter )
522     aWriter:flush()
523
524     return function()
525         local HTTPRequest = require( 'HTTPRequest' )
526         local HTTPResponse = require( 'HTTPResponse' )
527         local aRequest = HTTPRequest( aReader )
528         local aResponse = HTTPResponse()
529         local shouldContinue = Dispatch( someHandlers, aRequest, aResponse )
530        
531         aResponse( aWriter )
532  
533         return shouldContinue
534     end
535 end
536
537 function meta:__call( aReader, aWriter )
538     local aReader = aReader or io.stdin
539     local aWriter = aWriter or io.stdout
540     local aCall = Call( self, aReader, aWriter )
541     local aStatus, aResult = xpcall( aCall, debug.traceback )
542    
543     if not aStatus then
544         local HTTPResponse = require( 'HTTPResponse' )
545         local aResponse = HTTPResponse()
546        
547         io.stderr:write( tostring( aResult ), '\n' )
548        
549         aResponse.status.code = 500
550         aResponse.status.description = 'Internal Server Error'
551         aResponse.content = ( '%d %s' ):format( aResponse.status.code, aResponse.status.description )       
552         aResponse.header[ 'connection' ] = 'close'
553         aResponse.header[ 'content-type' ] = 'text/plain'
554         aResponse.header[ 'content-length' ] = aResponse.content:len()
555
556         aResponse( aWriter )
557        
558         return self
559     elseif not aResult then
560         return self
561     end
562        
563     return self( aReader, aWriter )
564 end
565
566 function meta:__concat( aValue )
567     return tostring( self ) .. tostring( aValue )
568 end
569
570 function meta:__tostring()
571     return _NAME
572 end
573
574 function meta:__index( aKey )
575     if aKey == 'address' then
576         return os.getenv( 'TCPLOCALHOST' ) or os.getenv( 'TCPLOCALIP' )
577     elseif aKey == 'port' then
578         return tonumber( os.getenv( 'TCPLOCALPORT' ) )
579     end
580 end
581
582 function meta:__newindex( aKey, aValue )
583     if type( aKey ) == 'table' then
584         if #aKey == 1 then
585             aKey = { nil, nil, aKey[ 1 ] }
586         elseif #aKey == 2 then
587             aKey = { aKey[ 1 ], nil, aKey[ 2 ] }
588         end
589     else
590         aKey = { nil, nil, aKey }
591     end
592    
593     rawset( self, #self + 1, { Pattern( unpack( aKey, 1, 3 ) ), aValue } )
594 end
Note: See TracBrowser for help on using the browser.