Changeset 7022


Ignore:
Timestamp:
14/06/10 16:36:56 (9 years ago)
Author:
pjkersha
Message:

Incomplete - task 9: Data Browser Replacement:

  • Tested correct retrieval for sub-dir trees
  • Tested for forbidden access and file not found

Next: add regular expression filtering to exposing unwanted files over HTTP interface

Location:
ceda_http_fileserver/trunk/ceda_http_fileserver/ceda/server/wsgi/fileserver
Files:
2 edited
2 copied
2 moved

Legend:

Unmodified
Added
Removed
  • ceda_http_fileserver/trunk/ceda_http_fileserver/ceda/server/wsgi/fileserver/app.py

    r7021 r7022  
    2727import os 
    2828import traceback 
     29import mimetypes 
     30import errno 
    2931import logging 
    30 import mimetypes 
    3132 
    3233log = logging.getLogger(__name__) 
    33  
    34 # Content type sources taken from http://en.wikipedia.org/wiki/MIME_type 
    35 _types_map = { 
    36     'js': 'application/x-javascript',  
    37     'html': 'text/html; charset=utf-8', 
    38     'fallback':'text/plain; charset=utf-8',  
    39     'ogg': 'application/ogg',  
    40     'xhtml':'text/html; charset=utf-8',  
    41     'rm':'audio/vnd.rn-realaudio',  
    42     'swf':'application/x-shockwave-flash',  
    43     'mp3': 'audio/mpeg',  
    44     'wma':'audio/x-ms-wma',  
    45     'ra':'audio/vnd.rn-realaudio',  
    46     'wav':'audio/x-wav',  
    47     'gif':'image/gif',  
    48     'jpeg':'image/jpeg', 
    49     'jpg':'image/jpeg',  
    50     'png':'image/png',  
    51     'tiff':'image/tiff',  
    52     'css':'text/css; charset=utf-8', 
    53     'mpeg':'video/mpeg',  
    54     'mp4':'video/mp4',  
    55     'qt':'video/quicktime',  
    56     'mov':'video/quicktime', 
    57     'wmv':'video/x-ms-wmv',  
    58     'atom':'application/atom+xml; charset=utf-8', 
    59     'xslt':'application/xslt+xml',  
    60     'svg':'image/svg+xml', 'mathml':'application/mathml+xml',  
    61     'rss':'application/rss+xml; charset=utf-8', 
    62     'ics':'text/calendar; charset=utf-8 ' 
    63 } 
    6434 
    6535 
     
    6838    from urllib import quote 
    6939    url = environ['wsgi.url_scheme']+'://' 
    70     if environ.get('HTTP_HOST'): url += environ['HTTP_HOST'] 
     40    if environ.get('HTTP_HOST'):  
     41        url += environ['HTTP_HOST'] 
    7142    else: 
    7243        url += environ['SERVER_NAME'] 
     
    7748            if environ['SERVER_PORT'] != '80': 
    7849               url += ':' + environ['SERVER_PORT'] 
    79     url += quote(environ.get('SCRIPT_NAME','')) 
    80     url += quote(environ.get('PATH_INFO','')).replace(url.replace(':', '%3A'), '') 
     50    url += urllib.quote(environ.get('SCRIPT_NAME','')) 
     51    url += urllib.quote(environ.get('PATH_INFO','')).replace(url.replace(':', '%3A'), '') 
    8152    if environ.get('QUERY_STRING'): 
    8253        url += '?' + environ['QUERY_STRING'] 
     
    11990 
    12091 
     92# Map HTTP status code to code + standard message string 
     93_statusCode2Msg = lambda code: "%d %s" % (code,  
     94                    httplib.responses.get(code, httplib.INTERNAL_SERVER_ERROR)) 
     95             
     96             
    12197class FileServerApp(object): 
    12298    """Application to serve static content""" 
     
    124100    DEFAULT_READ_BLK_SIZE = 1024 
    125101    DEFAULT_WRITE_BLK_SIZE = 1024 
     102    DEFAULT_CONTENT_TYPE = 'text/plain; charset=utf-8' 
     103     
     104    statusCode2Msg = staticmethod(_statusCode2Msg) 
     105     
     106    # Map HTTP status string to a given file system file access error 
     107    FILE_ACCESS_ERROR_MAP = { 
     108         errno.ENOENT: _statusCode2Msg(httplib.NOT_FOUND), 
     109         errno.EACCES: _statusCode2Msg(httplib.FORBIDDEN) 
     110    } 
    126111     
    127112    __slots__ = ( 
     
    129114        '__readBlkSize', 
    130115        '__httpMethodMap', 
    131         'fileSysPath', 
     116        'fileSysRoot', 
    132117        'mountPoint', 
    133118        'mimeTypes' 
    134119    ) 
    135120     
     121     
    136122    def __init__(self, root_path, mountPoint=None): 
    137123         
     
    140126        self.__httpMethodMap = None 
    141127         
    142         self.fileSysPath = os.path.abspath(os.path.expanduser(root_path)) 
     128        self.fileSysRoot = os.path.abspath(os.path.expanduser(root_path)) 
    143129        self.mountPoint = mountPoint 
    144130         
     
    229215            relativeURI = url[2] 
    230216         
    231         fileSysRelPath = urllib.url2pathname(relativeURI) 
     217        fileSysSubDir = urllib.url2pathname(relativeURI) 
     218         
     219        # This if statement stops os.path.join doing a join with an  
     220        # absolute path '/...'.  If this is done, the first argument is  
     221        # obliterated and the result is '/' exposing the root file system to 
     222        # the web client!! 
     223        if fileSysSubDir.startswith('/'): 
     224            fileSysSubDir = fileSysSubDir[1:] 
     225                 
    232226         
    233227        requestMethodName = environ['REQUEST_METHOD'] 
     
    236230            response = ('%r HTTP request method is not supported' %  
    237231                        requestMethodName) 
    238             status = "%d %s" % (httplib.METHOD_NOT_ALLOWED, 
    239                                 httplib.responses[httplib.METHOD_NOT_ALLOWED]) 
     232            status = FileServerApp.statusCode2Msg(httplib.METHOD_NOT_ALLOWED) 
    240233            start_response(status, 
    241234                           [('Content-length', str(len(response))), 
     
    243236            return [response] 
    244237             
    245         return requestMethod(self, fileSysRelPath, environ,  
    246                              start_response) 
    247          
    248     def do_get(self, fileSysRelPath, environ, start_response): 
    249         # This if statement stops os.path.join doing a join with the  
    250         # absolute path '/'.  If this is done, the first argument is  
    251         # obliterated and the result is '/' exposing the root file system to 
    252         # the web client!! 
    253         if fileSysRelPath == '/': 
    254             filePath = self.fileSysPath 
    255         else: 
    256             if fileSysRelPath.startswith('/'): 
    257                 fileSysRelPath = fileSysRelPath[1:] 
    258                  
    259             filePath = os.path.join(self.fileSysPath, fileSysRelPath) 
    260          
    261         isDir = os.path.isdir(filePath) 
    262         if isDir: 
     238        return requestMethod(self, fileSysSubDir, environ, start_response) 
     239         
     240    def do_get(self, fileSysSubDir, environ, start_response): 
     241        """HTTP GET callback""" 
     242        filePath = os.path.join(self.fileSysRoot, fileSysSubDir) 
     243         
     244        if os.path.isdir(filePath): 
    263245            dirContents = os.listdir(filePath) 
    264246 
    265247            lines = [ 
    266                 '<a href="%s">%s</a>' %  
    267                 (urljoin(fileSysRelPath, urllib.pathname2url(filename)), 
    268                 filename)  
     248                '<a href="/%s">%s</a>' %  
     249                (urllib.pathname2url(os.path.join(fileSysSubDir, filename)), 
     250                 filename)  
    269251                for filename in dirContents 
    270252            ] 
     
    282264                fileObj = open(filePath, 'rb') 
    283265                log.debug('opened file %s', filePath) 
    284             except IOError: 
     266            except IOError, e: 
     267                # Map file access error to a HTTP response code 
     268                status = FileServerApp.mapFileAccessError2HttpStatus(e.errno) 
    285269                log.error('failed to open file %r: %s', filePath,  
    286270                          traceback.format_exc()) 
    287                 response = '404 Not Found' 
    288                 start_response('404 Not found',  
     271                response = status 
     272                start_response(status,  
    289273                               [('Content-Type', 'text/plain'), 
    290274                                ('Content-length', str(len(response)))]) 
     
    300284            return response 
    301285         
    302     def do_put(self, fileSysRelPath, environ, start_response): 
    303         #Write file 
     286    def do_put(self, fileSysSubDir, environ, start_response): 
     287        """HTTP PUT callback""" 
    304288        try: 
    305             filePath = os.path.join(self.fileSysPath, fileSysRelPath) 
    306             f = open(filePath, 'w') 
    307             log.debug('opened file for writing %s' % filePath) 
    308         except: 
     289            filePath = os.path.join(self.fileSysRoot, fileSysSubDir) 
     290            fileObj = open(filePath, 'wb') 
     291            log.debug('opened file for writing %s', filePath) 
     292             
     293        except IOError, e: 
    309294            log.error('failed to open file for writing %r', filePath) 
    310             response = '403 Forbidden' 
    311             start_response('403 Forbidden',  
     295            status = FileServerApp.mapFileAccessError2HttpStatus(e.errno) 
     296            response = status 
     297            start_response(status,  
    312298                           [('Content-Type', 'text/plain'), 
    313299                            ('Content-length', str(len(response)))]) 
     
    325311        for length in readArray: 
    326312            inputBlk = inputStream.read(length) 
    327             f.write(inputBlk) 
    328  
     313            fileObj.write(inputBlk) 
     314 
     315    @classmethod 
     316    def mapFileAccessError2HttpStatus(cls, errno): 
     317        """Map file access error to a standard HTTP response status message""" 
     318        return cls.FILE_ACCESS_ERROR_MAP.get(errno,  
     319                            cls.statusCode2Msg(httplib.INTERNAL_SERVER_ERROR)) 
     320                                                         
    329321    def getContentType(self, path_info): 
    330322        """Make a best guess at the content type""" 
     
    333325            return contentType 
    334326        else: 
    335             return self.mimeTypes.types_map['fallback'] 
     327            return self.__class__.DEFAULT_CONTENT_TYPE 
    336328             
    337329    def __call__(self, environ, start_response): 
  • ceda_http_fileserver/trunk/ceda_http_fileserver/ceda/server/wsgi/fileserver/test/test_fileserver.py

    r7021 r7022  
    1515import unittest 
    1616from os import path 
    17  
     17import os 
    1818import httplib 
    1919import urllib 
     
    2929    HTDOCS_DIRNAME = 'htdocs' 
    3030    HTDOCS_DIR = path.join(THIS_DIR, HTDOCS_DIRNAME) 
    31     PNG_FILENAME = 'a png with uppercase suffix.PNG' 
    32     JPEG_FILENAME = 'my test jpeg.jpg' 
    33     PLAIN_TEXT_FILENAME = 'plain-text.file.txt' 
    34     HTML_FILENAME = 'my file.html' 
     31    HTDOCS_SUBDIR1 = 'sub.dir-1' 
     32    HTDOCS_SUBDIR2 = path.join('sub.dir-1', '.sub-dir2') 
     33    HTDOCS_SUBDIR3 = path.join('sub.dir-1', 'SubDir 3') 
     34     
     35    PNG_REL_FILEPATH = path.join(HTDOCS_SUBDIR1, 
     36                                 'a png with uppercase suffix.PNG') 
     37    PNG_REL_URIPATH = '/' + urllib.pathname2url(PNG_REL_FILEPATH) 
     38     
     39    JPEG_REL_FILEPATH = 'my test jpeg.jpg' 
     40    JPEG_REL_URIPATH = '/' + urllib.pathname2url(JPEG_REL_FILEPATH) 
     41     
     42    PLAIN_TEXT_REL_FILEPATH = 'plain-text.file.txt' 
     43    PLAIN_TEXT_REL_URIPATH = '/' + urllib.pathname2url(PLAIN_TEXT_REL_FILEPATH) 
     44     
     45    HTML_REL_FILEPATH = 'my file.html' 
     46    HTML_REL_URIPATH = '/' + urllib.pathname2url(HTML_REL_FILEPATH) 
     47     
    3548    PDF_FILENAME = 'a test PDF.pdf' 
     49    PDF_REL_FILEPATH = path.join(HTDOCS_SUBDIR3, PDF_FILENAME) 
     50    PDF_REL_URIPATH = '/' + urllib.pathname2url(PDF_REL_FILEPATH) 
    3651     
    3752    def __init__(self, *args, **kwargs): 
     
    4358    def test01Assert(self): 
    4459        response = self.app.get('/', status=200) 
    45         print(response) 
    46         self.assert_(response) 
     60        print(response.body) 
     61        self.assert_(response.body) 
     62        self.assert_(self.__class__.HTML_REL_FILEPATH in response.body) 
    4763 
    4864    def test02CheckBlkSizeValidation(self): 
     
    6581    def test05GetJPEG(self): 
    6682        # Test jpeg is returned with correct content type 
    67         response = self.app.get(urllib.pathname2url('/my test jpeg.jpg'),  
     83        response = self.app.get(self.__class__.JPEG_REL_URIPATH,  
    6884                                status=httplib.OK) 
    6985        print(response.headers) 
     
    7389    def test06GetPngWithUppercaseSuffix(self): 
    7490        # Test PNG is returned with correct content type 
    75         response = self.app.get(urllib.pathname2url( 
    76                                             '/a png with uppercase suffix.PNG'),  
     91        response = self.app.get(self.__class__.PNG_REL_URIPATH,  
    7792                                status=httplib.OK) 
    7893        print(response.headers) 
     
    8499        self.fileServerApp.writeBlkSize = 8 
    85100        localFilePath = path.join(self.__class__.HTDOCS_DIR,  
    86                                 'a png with uppercase suffix.PNG') 
     101                                  self.__class__.PNG_REL_FILEPATH) 
    87102        fileContent = open(localFilePath, 'rb').read() 
    88         response = self.app.get(urllib.pathname2url( 
    89                                             '/a png with uppercase suffix.PNG'),  
     103        response = self.app.get(self.__class__.PNG_REL_URIPATH,  
    90104                                status=httplib.OK) 
    91105        print(response.headers) 
     
    94108        self.assert_(response.body == fileContent) 
    95109 
     110    def test08FileNotFound(self): 
     111        response = self.app.get('/abc', status=httplib.NOT_FOUND) 
     112        print(response.headers) 
     113        print(response.body) 
     114 
     115    def test09ListSubDir(self): 
     116        # list content of a sub-directory 
     117        subDirUriPath = '/' + urllib.pathname2url(self.__class__.HTDOCS_SUBDIR3) 
     118        response = self.app.get(subDirUriPath, status=httplib.OK) 
     119        print(response.body) 
     120        self.assert_(self.__class__.PDF_FILENAME in response.body) 
     121 
     122    def test10PathFilter(self): 
     123        # Test filter which filters out path matching a given regular expression 
     124        pass 
     125      
     126    def test11AccessForbidden(self): 
     127         
     128        jpegFilePath = path.join(self.__class__.HTDOCS_DIR, 
     129                                 self.__class__.JPEG_REL_FILEPATH) 
     130        try: 
     131            # Get file permissions for the file 
     132            mode = os.stat(jpegFilePath).st_mode 
     133             
     134            # Make file inaccessible 
     135            os.chmod(jpegFilePath, 0000) 
     136 
     137            response = self.app.get(self.__class__.JPEG_REL_URIPATH,  
     138                                    status=httplib.FORBIDDEN) 
     139            print(response.headers) 
     140            print(response.body) 
     141             
     142        finally: 
     143            # Restore the original file permissions 
     144            os.chmod(jpegFilePath, mode) 
    96145     
     146    def test12WithAltMountPoint(self): 
     147        # Test with alternative mount point to the default '/' 
     148        self.fileServerApp.mountPoint = '/file-server' 
     149                
     150         
    97151class FileServerAppTestCaseBase(unittest.TestCase):   
    98152    """Base class for common Paste Deploy related set-up""" 
Note: See TracChangeset for help on using the changeset viewer.