root/HTTP/HTTP.lua

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