# -*- Mode: Python; tab-width: 4 -*- # # Author: Sam Rushing # Copyright 1997 by Sam Rushing # All Rights Reserved. # RCS_ID = '$Id: default_handler.py,v 1.2 2000/09/09 22:44:06 adamf Exp $' # standard python modules import os import regex import posixpath import stat import string import time # medusa modules import http_date import http_server import mime_type_table import status_handler import producers # from _quoteprog = regex.compile('%[0-9a-fA-F][0-9a-fA-F]') def unquote(s): i = 0 n = len(s) res = [] while 0 <= i < n: j = _quoteprog.search(s, i) if j < 0: res.append(s[i:]) break res.append(s[i:j] + chr(string.atoi(s[j+1:j+3], 16))) i = j+3 return string.join (res, '') # split a uri # ;?# path_regex = regex.compile ( # path params query fragment '\\([^;?#]*\\)\\(;[^?#]*\\)?\\(\\?[^#]*\)?\(#.*\)?' ) def split_path (path): if path_regex.match (path) != len(path): raise ValueError, "bad path" else: return map (lambda i,r=path_regex: r.group(i), range(1,5)) # This is the 'default' handler. it implements the base set of # features expected of a simple file-delivering HTTP server. file # services are provided through a 'filesystem' object, the very same # one used by the FTP server. # # You can replace or modify this handler if you want a non-standard # HTTP server. You can also derive your own handler classes from # it. # # support for handling POST requests is available in the derived # class , defined below. # from counter import counter class default_handler: valid_commands = ['get', 'head'] IDENT = 'Default HTTP Request Handler' # Pathnames that are tried when a URI resolves to a directory name directory_defaults = [ 'index.html', 'default.html' ] default_file_producer = producers.file_producer def __init__ (self, filesystem): self.filesystem = filesystem # count total hits self.hit_counter = counter() # count file deliveries self.file_counter = counter() # count cache hits self.cache_counter = counter() hit_counter = 0 def __repr__ (self): return '<%s (%s hits) at %x>' % ( self.IDENT, self.hit_counter, id (self) ) # always match, since this is a default def match (self, request): return 1 # handle a file request, with caching. def handle_request (self, request): if request.command not in self.valid_commands: request.error (400) # bad request return self.hit_counter.increment() [path, params, query, fragment] = split_path (request.uri) # unquote path if necessary (thanks to Skip Montaro for pointing # out that we must unquote in piecemeal fashion). if '%' in path: path = unquote (path) # strip off all leading slashes while path and path[0] == '/': path = path[1:] if self.filesystem.isdir (path): if path and path[-1] != '/': request['Location'] = 'http://%s/%s/' % ( request.channel.server.server_name, path ) request.error (301) return # we could also generate a directory listing here, # may want to move this into another method for that # purpose found = 0 if path and path[-1] != '/': path = path + '/' for default in self.directory_defaults: p = path + default if self.filesystem.isfile (p): path = p found = 1 break if not found: request.error (404) # Not Found return elif not self.filesystem.isfile (path): request.error (404) # Not Found return file_length = self.filesystem.stat (path)[stat.ST_SIZE] ims = get_header (IF_MODIFIED_SINCE, request.header) length_match = 1 if ims: length = IF_MODIFIED_SINCE.group(4) if length: try: length = string.atoi (length) if length != file_length: length_match = 0 except: pass ims_date = 0 if ims: ims_date = http_date.parse_http_date (ims) try: mtime = self.filesystem.stat (path)[stat.ST_MTIME] except: request.error (404) return if length_match and ims_date: if mtime <= ims_date: request.reply_code = 304 request.done() self.cache_counter.increment() return try: file = self.filesystem.open (path, 'rb') except IOError: request.error (404) return request['Last-Modified'] = http_date.build_http_date (mtime) request['Content-Length'] = file_length self.set_content_type (path, request) if request.command == 'get': request.push (self.default_file_producer (file)) self.file_counter.increment() request.done() def set_content_type (self, path, request): ext = string.lower (get_extension (path)) if mime_type_table.content_type_map.has_key (ext): request['Content-Type'] = mime_type_table.content_type_map[ext] else: # TODO: test a chunk off the front of the file for 8-bit # characters, and use application/octet-stream instead. request['Content-Type'] = 'text/plain' def status (self): return producers.simple_producer ( '
  • %s' % status_handler.html_repr (self) + '
      ' + '
    • Total Hits: %s' % self.hit_counter + '
    • Files Delivered: %s' % self.file_counter + '
    • Cache Hits: %s' % self.cache_counter + '
    ' ) ACCEPT = regex.compile ('Accept: \(.*\)', regex.casefold) # HTTP/1.0 doesn't say anything about the "; length=nnnn" addition # to this header. I suppose it's purpose is to avoid the overhead # of parsing dates... IF_MODIFIED_SINCE = regex.compile ( 'If-Modified-Since: \([^;]+\)\(\(; length=\([0-9]+\)$\)\|$\)', regex.casefold ) USER_AGENT = regex.compile ('User-Agent: \(.*\)', regex.casefold) boundary_chars = "A-Za-z0-9'()+_,./:=?-" CONTENT_TYPE = regex.compile ( 'Content-Type: \([^;]+\)\(\(; boundary=\([%s]+\)$\)\|$\)' % boundary_chars, regex.casefold ) get_header = http_server.get_header def get_extension (path): dirsep = string.rfind (path, '/') dotsep = string.rfind (path, '.') if dotsep > dirsep: return path[dotsep+1:] else: return ''