root/HTTP/HTTPExtra.lua

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