Commit d844c004 authored by Amos Latteier's avatar Amos Latteier

added more of the medusa distribution as needed.

parent 1ddf95c0
# -*- Mode: Python; tab-width: 4 -*-
# It is tempting to add an __int__ method to this class, but it's not
# a good idea. This class tries to gracefully handle integer
# overflow, and to hide this detail from both the programmer and the
# user. Note that the __str__ method can be relied on for printing out
# the value of a counter:
#
# >>> print 'Total Client: %s' % self.total_clients
#
# If you need to do arithmetic with the value, then use the 'as_long'
# method, the use of long arithmetic is a reminder that the counter
# will overflow.
class counter:
"general-purpose counter"
def __init__ (self, initial_value=0):
self.value = initial_value
def increment (self, delta=1):
result = self.value
try:
self.value = self.value + delta
except OverflowError:
self.value = long(self.value) + delta
return result
def decrement (self, delta=1):
result = self.value
try:
self.value = self.value - delta
except OverflowError:
self.value = long(self.value) - delta
return result
def as_long (self):
return long(self.value)
def __nonzero__ (self):
return self.value != 0
def __repr__ (self):
return '<counter value=%s at %x>' % (self.value, id(self))
def __str__ (self):
return str(long(self.value))[:-1]
# -*- Mode: Python; tab-width: 4 -*-
#
# Author: Sam Rushing <rushing@nightmare.com>
# Copyright 1997 by Sam Rushing
# All Rights Reserved.
#
# This software is provided free for non-commercial use. If you are
# interested in using this software in a commercial context, or in
# purchasing support, please contact the author.
RCS_ID = '$Id: default_handler.py,v 1.1 1999/01/09 03:17:31 amos 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 <lib/urllib.py>
_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>;<params>?<query>#<fragment>
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 <default_with_post_handler>, 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 = 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 (
'<li>%s' % status_handler.html_repr (self)
+ '<ul>'
+ ' <li><b>Total Hits:</b> %s' % self.hit_counter
+ ' <li><b>Files Delivered:</b> %s' % self.file_counter
+ ' <li><b>Cache Hits:</b> %s' % self.cache_counter
+ '</ul>'
)
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 ''
# -*- Mode: Python; tab-width: 4 -*-
import regex
import string
import time
def concat (*args):
return string.joinfields (args, '')
def join (seq, field=' '):
return string.joinfields (seq, field)
def group (s):
return '\\(' + s + '\\)'
short_days = ['sun','mon','tue','wed','thu','fri','sat']
long_days = ['sunday','monday','tuesday','wednesday','thursday','friday','saturday']
short_day_reg = group (join (short_days, '\\|'))
long_day_reg = group (join (long_days, '\\|'))
daymap = {}
for i in range(7):
daymap[short_days[i]] = i
daymap[long_days[i]] = i
hms_reg = join (3 * [group('[0-9][0-9]')], ':')
months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec']
monmap = {}
for i in range(12):
monmap[months[i]] = i+1
months_reg = group (join (months, '\\|'))
# From draft-ietf-http-v11-spec-07.txt/3.3.1
# Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123
# Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
# Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
# rfc822 format
rfc822_date = join (
[concat (short_day_reg,','), # day
group('[0-9][0-9]?'), # date
months_reg, # month
group('[0-9]+'), # year
hms_reg, # hour minute second
'gmt'
],
' '
)
rfc822_reg = regex.compile (rfc822_date)
def unpack_rfc822 ():
g = rfc822_reg.group
a = string.atoi
return (
a(g(4)), # year
monmap[g(3)], # month
a(g(2)), # day
a(g(5)), # hour
a(g(6)), # minute
a(g(7)), # second
0,
0,
0
)
# rfc850 format
rfc850_date = join (
[concat (long_day_reg,','),
join (
[group ('[0-9][0-9]?'),
months_reg,
group ('[0-9]+')
],
'-'
),
hms_reg,
'gmt'
],
' '
)
rfc850_reg = regex.compile (rfc850_date)
# they actually unpack the same way
def unpack_rfc850 ():
g = rfc850_reg.group
a = string.atoi
return (
a(g(4)), # year
monmap[g(3)], # month
a(g(2)), # day
a(g(5)), # hour
a(g(6)), # minute
a(g(7)), # second
0,
0,
0
)
# parsdate.parsedate - ~700/sec.
# parse_http_date - ~1333/sec.
def build_http_date (when):
return time.strftime ('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(when))
def parse_http_date (d):
d = string.lower (d)
# Thanks to Craig Silverstein <csilvers@google.com> for pointing
# out the DST discrepancy
if time.daylight:
tz = time.altzone
else:
tz = time.timezone
# rfc850 comes first, netscape uses it. <frown>
if rfc850_reg.match (d) == len(d):
return int (time.mktime (unpack_rfc850()) - tz)
elif rfc822_reg.match (d) == len(d):
return int (time.mktime (unpack_rfc822()) - tz)
else:
return 0
#! /usr/local/bin/python
# -*- Mode: Python; tab-width: 4 -*-
#
# Author: Sam Rushing <rushing@nightmare.com>
# Copyright 1996, 1997 by Sam Rushing
# All Rights Reserved.
#
# This software is provided free for non-commercial use. If you are
# interested in using this software in a commercial context, or in
# purchasing support, please contact the author.
RCS_ID = '$Id: http_server.py,v 1.1 1999/01/09 03:17:32 amos Exp $'
# python modules
import os
import regex
import socket
import stat
import string
import sys
import time
# async modules
import asyncore
import asynchat
# medusa modules
import http_date
import producers
import status_handler
import logger
VERSION_STRING = string.split(RCS_ID)[2]
from counter import counter
# ===========================================================================
# Request Object
# ===========================================================================
class http_request:
# default reply code
reply_code = 200
request_counter = counter()
# Whether to automatically use chunked encoding when
#
# HTTP version is 1.1
# Content-Length is not set
# Chunked encoding is not already in effect
#
# If your clients are having trouble, you might want to disable this.
use_chunked = 1
# by default, this request object ignores user data.
collector = None
def __init__ (self, *args):
# unpack information about the request
(self.channel, self.request,
self.command, self.uri, self.version,
self.header) = args
self.outgoing = fifo()
self.reply_headers = {
'Server' : 'Medusa/%s' % VERSION_STRING,
'Date' : http_date.build_http_date (time.time())
}
self.request_number = http_request.request_counter.increment()
# --------------------------------------------------
# reply header management
# --------------------------------------------------
def __setitem__ (self, key, value):
self.reply_headers[key] = value
def __getitem__ (self, key):
return self.reply_headers[key]
def has_key (self, key):
return self.reply_headers.has_key (key)
def build_reply_header (self):
return string.join (
[self.response(self.reply_code)] + map (
lambda x: '%s: %s' % x,
self.reply_headers.items()
),
'\r\n'
) + '\r\n\r\n'
# --------------------------------------------------
# user data
# --------------------------------------------------
def collect_incoming_data (self, data):
if self.collector:
self.collector.collect_incoming_data (data)
else:
sys.stderr.write (
'warning: dropping %d bytes of incoming request data\n' % len(data)
)
def found_terminator (self):
if self.collector:
self.collector.collect_incoming_data (data)
else:
sys.stderr.write (
'warning: unexpected end-of-record for incoming request\n'
)
def push (self, producer):
if type(producer) == type(''):
self.outgoing.push (producers.simple_producer (producer))
else:
self.outgoing.push (producer)
def response (self, code=200):
message = self.responses[code]
self.reply_code = code
return 'HTTP/%s %d %s' % (self.version, code, message)
def error (self, code):
self.reply_code = code
message = self.responses[code]
s = self.DEFAULT_ERROR_MESSAGE % {
'code': code,
'message': message,
}
self['Content-Length'] = len(s)
self['Content-Type'] = 'text/html'
# make an error reply
self.push (s)
self.done()
# can also be used for empty replies
reply_now = error
def done (self):
"finalize this transaction - send output to the http channel"
# ----------------------------------------
# persistent connection management
# ----------------------------------------
# --- BUCKLE UP! ----
connection = string.lower (get_header (CONNECTION, self.header))
close_it = 0
wrap_in_chunking = 0
if self.version == '1.0':
if connection == 'keep-alive':
if not self.has_key ('Content-Length'):
close_it = 1
else:
self['Connection'] = 'Keep-Alive'
else:
close_it = 1
elif self.version == '1.1':
if connection == 'close':
close_it = 1
elif not self.has_key ('Content-Length'):
if self.has_key ('Transfer-Encoding'):
if not self['Transfer-Encoding'] == 'chunked':
close_it = 1
elif self.use_chunked:
self['Transfer-Encoding'] = 'chunked'
wrap_in_chunking = 1
else:
close_it = 1
outgoing_header = producers.simple_producer (self.build_reply_header())
if close_it:
self['Connection'] = 'close'
if wrap_in_chunking:
outgoing_producer = producers.chunked_producer (
producers.composite_producer (self.outgoing)
)
# prepend the header
outgoing_producer = producers.composite_producer (
fifo([outgoing_header, outgoing_producer])
)
else:
# prepend the header
self.outgoing.push_front (outgoing_header)
outgoing_producer = producers.composite_producer (self.outgoing)
# apply a few final transformations to the output
self.channel.push_with_producer (
# globbing gives us large packets
producers.globbing_producer (
# hooking lets us log the number of bytes sent
producers.hooked_producer (
outgoing_producer,
self.log
)
)
)
self.channel.current_request = None
if close_it:
self.channel.close_when_done()
def log (self, bytes):
print 'request %3d: %s %d bytes' % (
self.request_counter,
self.request,
bytes
)
def log_date_string (self, when):
return time.strftime (
'%d/%b/%Y:%H:%M:%S ',
time.gmtime(when)
) + tz_for_log
def log (self, bytes):
self.channel.server.logger.log (
self.channel.addr[0],
'%d - - [%s] "%s" %d %d\n' % (
self.channel.addr[1],
self.log_date_string (time.time()),
self.request,
self.reply_code,
bytes
)
)
responses = {
100: "Continue",
101: "Switching Protocols",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Moved Temporarily",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Time-out",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Request Entity Too Large",
414: "Request-URI Too Large",
415: "Unsupported Media Type",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Time-out",
505: "HTTP Version not supported"
}
# Default error message
DEFAULT_ERROR_MESSAGE = string.join (
['<head>',
'<title>Error response</title>',
'</head>',
'<body>',
'<h1>Error response</h1>',
'<p>Error code %(code)d.',
'<p>Message: %(message)s.',
'</body>',
''
],
'\r\n'
)
# ===========================================================================
# HTTP Channel Object
# ===========================================================================
class http_channel (asynchat.async_chat):
# use a larger default output buffer
ac_out_buffer_size = 1<<16
current_request = None
channel_counter = counter()
def __init__ (self, server, conn, addr):
self.channel_number = http_channel.channel_counter.increment()
self.request_counter = counter()
asynchat.async_chat.__init__ (self, conn)
self.server = server
self.addr = addr
self.set_terminator ('\r\n\r\n')
self.in_buffer = ''
self.creation_time = int (time.time())
self.check_maintenance()
def __repr__ (self):
ar = asynchat.async_chat.__repr__(self)[1:-1]
return '<%s channel#: %s requests:%s>' % (
ar,
self.channel_number,
self.request_counter
)
# Channel Counter, Maintenance Interval...
maintenance_interval = 500
def check_maintenance (self):
if not self.channel_number % self.maintenance_interval:
self.maintenance()
def maintenance (self):
self.kill_zombies()
# 30-minute zombie timeout. status_handler also knows how to kill zombies.
zombie_timeout = 30 * 60
def kill_zombies (self):
now = int (time.time())
for channel in asyncore.socket_map.keys():
if channel.__class__ == http_channel:
if (now - channel.creation_time) > channel.zombie_timeout:
channel.close()
# --------------------------------------------------
# send/recv overrides, good place for instrumentation.
# --------------------------------------------------
# this information needs to get into the request object,
# so that it may log correctly.
def send (self, data):
result = asynchat.async_chat.send (self, data)
self.server.bytes_out.increment (len(data))
return result
def recv (self, buffer_size):
result = asynchat.async_chat.recv (self, buffer_size)
self.server.bytes_in.increment (len(result))
return result
def log (self, *args):
pass
# --------------------------------------------------
# async_chat methods
# --------------------------------------------------
def collect_incoming_data (self, data):
if self.current_request:
# we are receiving data (probably POST data) for a request
self.current_request.collect_incoming_data (data)
else:
# we are receiving header (request) data
self.in_buffer = self.in_buffer + data
def found_terminator (self):
if self.current_request:
self.current_request.found_terminator()
else:
header = self.in_buffer
self.in_buffer = ''
lines = string.split (header, '\r\n')
# --------------------------------------------------
# crack the request header
# --------------------------------------------------
request = lines[0]
command, uri, version = crack_request (request)
header = join_headers (lines[1:])
r = http_request (self, request, command, uri, version, header)
self.request_counter.increment()
self.server.total_requests.increment()
# --------------------------------------------------
# handler selection and dispatch
# --------------------------------------------------
for h in self.server.handlers:
if h.match (r):
try:
self.current_request = r
# This isn't used anywhere.
# r.handler = h # CYCLE
h.handle_request (r)
except:
self.server.exceptions.increment()
t,v,tb = sys.exc_info()
(file,fun,line),tbinfo = asyncore.compact_traceback (t,v,tb)
while tb.tb_next:
tb = tb.tb_next
# Log this to a better place.
print 'Server Error: %s, %s: file: %s line: %s' % (t,v,file,line)
# IMPORTANT: without this <del>, this entire connection will leak. [why?]
del t,v,tb
try:
r.error (500)
except:
pass
return
# no handlers, so complain
r.error (404)
def writable (self):
# this is just the normal async_chat 'writable', here for comparison
return self.ac_out_buffer or len(self.producer_fifo)
def writable_for_proxy (self):
# this version of writable supports the idea of a 'stalled' producer
# [i.e., it's not ready to produce any output yet] This is needed by
# the proxy, which will be waiting for the magic combination of
# 1) hostname resolved
# 2) connection made
# 3) data available.
if self.ac_out_buffer:
return 1
elif len(self.producer_fifo):
p = self.producer_fifo.first()
if hasattr (p, 'stalled'):
return not p.stalled()
else:
return 1
# ===========================================================================
# HTTP Server Object
# ===========================================================================
class http_server (asyncore.dispatcher):
SERVER_IDENT = 'HTTP Server (V%s)' % VERSION_STRING
channel_class = http_channel
def __init__ (self, ip, port, resolver=None, logger_object=None):
self.ip = ip
self.port = port
asyncore.dispatcher.__init__ (self)
self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
self.handlers = []
if not logger_object:
logger_object = logger.file_logger (sys.stdout)
self.set_reuse_addr()
self.bind ((ip, port))
self.listen (5)
host, port = self.socket.getsockname()
if not ip:
print 'Warning: computing default hostname'
self.server_name = socket.gethostbyaddr (
socket.gethostbyname (socket.gethostname())
)[0]
else:
self.server_name = socket.gethostbyaddr (ip)[0]
self.server_port = port
self.total_clients = counter()
self.total_requests = counter()
self.exceptions = counter()
self.bytes_out = counter()
self.bytes_in = counter()
if not logger_object:
logger_object = logger.file_logger (sys.stdout)
if resolver:
self.logger = logger.resolving_logger (resolver, logger_object)
else:
self.logger = logger.unresolving_logger (logger_object)
sys.stdout.write (
'Medusa (V%s) started at %s'
'\n\tHostname: %s'
'\n\tPort:%d'
'\n' % (
VERSION_STRING,
time.ctime(time.time()),
self.server_name,
port,
)
)
def writable (self):
return 0
def handle_read (self):
pass
def readable (self):
return self.accepting
def handle_connect (self):
pass
def handle_accept (self):
self.total_clients.increment()
try:
conn, addr = self.accept()
except socket.error:
# linux: on rare occasions we get a bogus socket back from
# accept. socketmodule.c:makesockaddr complains that the
# address family is unknown. We don't want the whole server
# to shut down because of this.
sys.stderr.write ('warning: server accept() threw an exception\n')
return
self.channel_class (self, conn, addr)
def install_handler (self, handler, back=0):
if back:
self.handlers.append (handler)
else:
self.handlers.insert (0, handler)
def remove_handler (self, handler):
self.handlers.remove (handler)
def status (self):
def nice_bytes (n):
return string.join (status_handler.english_bytes (n))
handler_stats = filter (None, map (maybe_status, self.handlers))
if self.total_clients:
ratio = float(self.total_requests.as_long()/self.total_clients.as_long())
else:
ratio = 0.0
return producers.composite_producer (
fifo ([producers.lines_producer (
['<h2>%s</h2>' % self.SERVER_IDENT,
'<br>Listening on: <b>Host:</b> %s' % self.server_name,
'<b>Port:</b> %d' % self.port,
'<p><ul>'
'<li>Total <b>Clients:</b> %s' % self.total_clients,
'<b>Requests:</b> %s' % self.total_requests,
'<b>Requests/Client:</b> %.1f' % (ratio),
'<li>Total <b>Bytes In:</b> %s' % (nice_bytes (self.bytes_in.as_long())),
'<b>Bytes Out:</b> %s' % (nice_bytes (self.bytes_out.as_long())),
'<li>Total <b>Exceptions:</b> %s' % self.exceptions,
'</ul><p>'
'<b>Extension List</b><ul>',
])] + handler_stats + [producers.simple_producer('</ul>')]
)
)
def maybe_status (thing):
if hasattr (thing, 'status'):
return thing.status()
else:
return None
CONNECTION = regex.compile ('Connection: \(.*\)', regex.casefold)
# merge multi-line headers
# [486dx2: ~500/sec]
def join_headers (headers):
r = []
for i in range(len(headers)):
if headers[i][0] in ' \t':
r[-1] = r[-1] + headers[i][1:]
else:
r.append (headers[i])
return r
def get_header (head_reg, lines, group=1):
for line in lines:
if head_reg.match (line) == len(line):
return head_reg.group(group)
return ''
REQUEST = regex.compile ('\([^ ]+\) \([^ ]+\)\(\( HTTP/\([0-9.]+\)\)$\|$\)')
def crack_request (r):
if REQUEST.match (r) == len(r):
if REQUEST.group(3):
version = REQUEST.group(5)
else:
version = None
return string.lower (REQUEST.group (1)), REQUEST.group(2), version
class fifo:
def __init__ (self, list=None):
if not list:
self.list = []
else:
self.list = list
def __len__ (self):
return len(self.list)
def first (self):
return self.list[0]
def push_front (self, object):
self.list.insert (0, object)
def push (self, data):
self.list.append (data)
def pop (self):
if self.list:
result = self.list[0]
del self.list[0]
return (1, result)
else:
return (0, None)
def compute_timezone_for_log ():
if time.daylight:
tz = time.altzone
else:
tz = time.timezone
if tz > 0:
neg = 1
else:
neg = 0
tz = -tz
h, rem = divmod (tz, 3600)
m, rem = divmod (rem, 60)
if neg:
return '-%02d%02d' % (h, m)
else:
return '+%02d%02d' % (h, m)
# if you run this program over a TZ change boundary, this will be invalid.
tz_for_log = compute_timezone_for_log()
if __name__ == '__main__':
import sys
if len(sys.argv) < 2:
print 'usage: %s <root> <port>' % (sys.argv[0])
else:
import monitor
import filesys
import default_handler
import status_handler
import ftp_server
import chat_server
import resolver
import logger
rs = resolver.caching_resolver ('127.0.0.1')
lg = logger.file_logger (sys.stdout)
ms = monitor.secure_monitor_server ('fnord', '127.0.0.1', 9999)
fs = filesys.os_filesystem (sys.argv[1])
dh = default_handler.default_handler (fs)
hs = http_server ('', string.atoi (sys.argv[2]), rs, lg)
hs.install_handler (dh)
ftp = ftp_server.ftp_server (
ftp_server.dummy_authorizer(sys.argv[1]),
port=8021,
resolver=rs,
logger_object=lg
)
cs = chat_server.chat_server ('', 7777)
sh = status_handler.status_extension([hs,ms,ftp,cs,rs])
hs.install_handler (sh)
if ('-p' in sys.argv):
def profile_loop ():
try:
asyncore.loop()
except KeyboardInterrupt:
pass
import profile
profile.run ('profile_loop()', 'profile.out')
else:
asyncore.loop()
# -*- Mode: Python; tab-width: 4 -*-
import asynchat
import socket
import string
#
# three types of log:
# 1) file
# with optional flushing.
# 2) socket
# dump output directly to a socket connection. [how do we
# keep it open?]
# 3) syslog
# log to syslog via tcp. this is a per-line protocol.
#
#
# The 'standard' interface to a logging object is simply
# log_object.log (message)
#
# a file-like object that captures output, and
# makes sure to flush it always... this could
# be connected to:
# o stdio file
# o low-level file
# o socket channel
# o syslog output...
class file_logger:
# pass this either a path or a file object.
def __init__ (self, file, flush=1, mode='wa'):
if type(file) == type(''):
if (file == '-'):
import sys
self.file = sys.stdout
else:
self.file = open (file, mode)
else:
self.file = file
self.do_flush = flush
def __repr__ (self):
return '<file logger: %s>' % self.file
def write (self, data):
self.file.write (data)
self.maybe_flush()
def writeline (self, line):
self.file.writeline (line)
self.maybe_flush()
def writelines (self, lines):
self.file.writelines (lines)
self.maybe_flush()
def maybe_flush (self):
if self.do_flush:
self.file.flush()
def flush (self):
self.file.flush()
def softspace (self, *args):
pass
def log (self, message):
if message[-1] not in ('\r', '\n'):
self.write (message + '\n')
else:
self.write (message)
# syslog is a line-oriented log protocol - this class would be
# appropriate for FTP or HTTP logs, but not for dumping stderr to.
# TODO: a simple safety wrapper that will ensure that the line sent
# to syslog is reasonable.
# TODO: async version of syslog_client: now, log entries use blocking
# send()
import m_syslog
syslog_logger = m_syslog.syslog_client
class syslog_logger (m_syslog.syslog_client):
def __init__ (self, address, facility='user'):
m_syslog.syslog_client.__init__ (self, address)
self.facility = m_syslog.facility_names[facility]
self.address=address
def __repr__ (self):
return '<syslog logger address=%s>' % (repr(self.address))
def log (self, message):
m_syslog.syslog_client.log (
self,
message,
facility=self.facility,
priority=m_syslog.LOG_INFO
)
# log to a stream socket, asynchronously
class socket_logger (asynchat.async_chat):
def __init__ (self, address):
if type(address) == type(''):
self.create_socket (socket.AF_UNIX, socket.SOCK_STREAM)
else:
self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
self.connect (address)
self.address = address
def __repr__ (self):
return '<socket logger: address=%s>' % (self.address)
def log (self, message):
if message[-2:] != '\r\n':
self.socket.push (message + '\r\n')
else:
self.socket.push (message)
# log to multiple places
class multi_logger:
def __init__ (self, loggers):
self.loggers = loggers
def __repr__ (self):
return '<multi logger: %s>' % (repr(self.loggers))
def log (self, message):
for logger in self.loggers:
logger.log (message)
class resolving_logger:
"""Feed (ip, message) combinations into this logger to get a
resolved hostname in front of the message. The message will not
be logged until the PTR request finishes (or fails)."""
def __init__ (self, resolver, logger):
self.resolver = resolver
self.logger = logger
class logger_thunk:
def __init__ (self, message, logger):
self.message = message
self.logger = logger
def __call__ (self, host, ttl, answer):
if not answer:
answer = host
self.logger.log ('%s:%s' % (answer, self.message))
def log (self, ip, message):
self.resolver.resolve_ptr (
ip,
self.logger_thunk (
message,
self.logger
)
)
class unresolving_logger:
"Just in case you don't want to resolve"
def __init__ (self, logger):
self.logger = logger
def log (self, ip, message):
self.logger.log ('%s:%s' % (ip, message))
def strip_eol (line):
while line and line[-1] in '\r\n':
line = line[:-1]
return line
class tail_logger:
"Keep track of the last <size> log messages"
def __init__ (self, logger, size=500):
self.size = size
self.logger = logger
self.messages = []
def log (self, message):
self.messages.append (strip_eol (message))
if len (self.messages) > self.size:
del self.messages[0]
self.logger.log (message)
# -*- Mode: Python; tab-width: 4 -*-
# ======================================================================
# Copyright 1997 by Sam Rushing
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Sam
# Rushing not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# ======================================================================
"""socket interface to unix syslog.
On Unix, there are usually two ways of getting to syslog: via a
local unix-domain socket, or via the TCP service.
Usually "/dev/log" is the unix domain socket. This may be different
for other systems.
>>> my_client = syslog_client ('/dev/log')
Otherwise, just use the UDP version, port 514.
>>> my_client = syslog_client (('my_log_host', 514))
On win32, you will have to use the UDP version. Note that
you can use this to log to other hosts (and indeed, multiple
hosts).
This module is not a drop-in replacement for the python
<syslog> extension module - the interface is different.
Usage:
>>> c = syslog_client()
>>> c = syslog_client ('/strange/non_standard_log_location')
>>> c = syslog_client (('other_host.com', 514))
>>> c.log ('testing', facility='local0', priority='debug')
"""
# TODO: support named-pipe syslog.
# [see ftp://sunsite.unc.edu/pub/Linux/system/Daemons/syslog-fifo.tar.z]
# from <linux/sys/syslog.h>:
# ===========================================================================
# priorities/facilities are encoded into a single 32-bit quantity, where the
# bottom 3 bits are the priority (0-7) and the top 28 bits are the facility
# (0-big number). Both the priorities and the facilities map roughly
# one-to-one to strings in the syslogd(8) source code. This mapping is
# included in this file.
#
# priorities (these are ordered)
LOG_EMERG = 0 # system is unusable
LOG_ALERT = 1 # action must be taken immediately
LOG_CRIT = 2 # critical conditions
LOG_ERR = 3 # error conditions
LOG_WARNING = 4 # warning conditions
LOG_NOTICE = 5 # normal but significant condition
LOG_INFO = 6 # informational
LOG_DEBUG = 7 # debug-level messages
# facility codes
LOG_KERN = 0 # kernel messages
LOG_USER = 1 # random user-level messages
LOG_MAIL = 2 # mail system
LOG_DAEMON = 3 # system daemons
LOG_AUTH = 4 # security/authorization messages
LOG_SYSLOG = 5 # messages generated internally by syslogd
LOG_LPR = 6 # line printer subsystem
LOG_NEWS = 7 # network news subsystem
LOG_UUCP = 8 # UUCP subsystem
LOG_CRON = 9 # clock daemon
LOG_AUTHPRIV = 10 # security/authorization messages (private)
# other codes through 15 reserved for system use
LOG_LOCAL0 = 16 # reserved for local use
LOG_LOCAL1 = 17 # reserved for local use
LOG_LOCAL2 = 18 # reserved for local use
LOG_LOCAL3 = 19 # reserved for local use
LOG_LOCAL4 = 20 # reserved for local use
LOG_LOCAL5 = 21 # reserved for local use
LOG_LOCAL6 = 22 # reserved for local use
LOG_LOCAL7 = 23 # reserved for local use
priority_names = {
"alert": LOG_ALERT,
"crit": LOG_CRIT,
"debug": LOG_DEBUG,
"emerg": LOG_EMERG,
"err": LOG_ERR,
"error": LOG_ERR, # DEPRECATED
"info": LOG_INFO,
"notice": LOG_NOTICE,
"panic": LOG_EMERG, # DEPRECATED
"warn": LOG_WARNING, # DEPRECATED
"warning": LOG_WARNING,
}
facility_names = {
"auth": LOG_AUTH,
"authpriv": LOG_AUTHPRIV,
"cron": LOG_CRON,
"daemon": LOG_DAEMON,
"kern": LOG_KERN,
"lpr": LOG_LPR,
"mail": LOG_MAIL,
"news": LOG_NEWS,
"security": LOG_AUTH, # DEPRECATED
"syslog": LOG_SYSLOG,
"user": LOG_USER,
"uucp": LOG_UUCP,
"local0": LOG_LOCAL0,
"local1": LOG_LOCAL1,
"local2": LOG_LOCAL2,
"local3": LOG_LOCAL3,
"local4": LOG_LOCAL4,
"local5": LOG_LOCAL5,
"local6": LOG_LOCAL6,
"local7": LOG_LOCAL7,
}
import socket
class syslog_client:
def __init__ (self, address='/dev/log'):
self.address = address
if type (address) == type(''):
self.socket = socket.socket (socket.AF_UNIX, socket.SOCK_STREAM)
self.socket.connect (address)
self.unix = 1
else:
self.socket = socket.socket (socket.AF_INET, socket.SOCK_DGRAM)
self.unix = 0
# curious: when talking to the unix-domain '/dev/log' socket, a
# zero-terminator seems to be required. this string is placed
# into a class variable so that it can be overridden if
# necessary.
log_format_string = '<%d>%s\000'
def log (self, message, facility=LOG_USER, priority=LOG_INFO):
message = self.log_format_string % (
self.encode_priority (facility, priority),
message
)
if self.unix:
self.socket.send (message)
else:
self.socket.sendto (message, self.address)
def encode_priority (self, facility, priority):
if type(facility) == type(''):
facility = facility_names[facility]
if type(priority) == type(''):
priority = priority_names[priority]
return (facility<<3) | priority
def close (self):
if self.unix:
self.socket.close()
# -*- Mode: Python -*-
# the medusa icon as a python source file.
width = 97
height = 61
data = 'GIF89aa\000=\000\204\000\000\000\000\000\255\255\255\245\245\245ssskkkccc111)))\326\326\326!!!\316\316\316\300\300\300\204\204\000\224\224\224\214\214\214\200\200\200RRR\377\377\377JJJ\367\367\367BBB\347\347\347\000\204\000\020\020\020\265\265\265\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000!\371\004\001\000\000\021\000,\000\000\000\000a\000=\000\000\005\376`$\216di\236h\252\256l\353\276p,\317tm\337x\256\357|m\001@\240E\305\000\364\2164\206R)$\005\201\214\007r\012{X\255\312a\004\260\\>\026\3240\353)\224n\001W+X\334\373\231~\344.\303b\216\024\027x<\273\307\255G,rJiWN\014{S}k"?ti\013EdPQ\207G@_%\000\026yy\\\201\202\227\224<\221Fs$pOjWz\241<r@vO\236\231\233k\247M\2544\203F\177\235\236L#\247\256Z\270,\266BxJ[\276\256A]iE\304\305\262\273E\313\201\275i#\\\303\321\'h\203V\\\177\326\276\216\220P~\335\230_\264\013\342\275\344KF\233\360Q\212\352\246\000\367\274s\361\236\334\347T\341;\341\246\2202\177\3142\211`\242o\325@S\202\264\031\252\207\260\323\256\205\311\036\236\270\002\'\013\302\177\274H\010\324X\002\0176\212\037\376\321\360\032\226\207\244\2674(+^\202\346r\205J\0211\375\241Y#\256f\0127\315>\272\002\325\307g\012(\007\205\312#j\317(\012A\200\224.\241\003\346GS\247\033\245\344\264\366\015L\'PXQl]\266\263\243\232\260?\245\316\371\362\225\035\332\243J\273\332Q\263\357-D\241T\327\270\265\013W&\330\010u\371b\322IW0\214\261]\003\033Va\365Z#\207\213a\030k\2647\262\014p\354\024[n\321N\363\346\317\003\037P\000\235C\302\000\3228(\244\363YaA\005\022\255_\237@\260\000A\212\326\256qbp\321\332\266\011\334=T\023\010"!B\005\003A\010\224\020\220 H\002\337#\020 O\276E\357h\221\327\003\\\000b@v\004\351A.h\365\354\342B\002\011\257\025\\ \220\340\301\353\006\000\024\214\200pA\300\353\012\364\241k/\340\033C\202\003\000\310fZ\011\003V\240R\005\007\354\376\026A\000\000\360\'\202\177\024\004\210\003\000\305\215\360\000\000\015\220\240\332\203\027@\'\202\004\025VpA\000%\210x\321\206\032J\341\316\010\262\211H"l\333\341\200\200>"]P\002\212\011\010`\002\0066FP\200\001\'\024p]\004\027(8B\221\306]\000\201w>\002iB\001\007\340\260"v7J1\343(\257\020\251\243\011\242i\263\017\215\337\035\220\200\221\365m4d\015\016D\251\341iN\354\346Ng\253\200I\240\031\35609\245\2057\311I\302\2007t\231"&`\314\310\244\011e\226(\236\010w\212\300\234\011\012HX(\214\253\311@\001\233^\222pg{% \340\035\224&H\000\246\201\362\215`@\001"L\340\004\030\234\022\250\'\015(V:\302\235\030\240q\337\205\224\212h@\177\006\000\250\210\004\007\310\207\337\005\257-P\346\257\367]p\353\203\271\256:\203\236\211F\340\247\010\3329g\244\010\307*=A\000\203\260y\012\304s#\014\007D\207,N\007\304\265\027\021C\233\207%B\366[m\353\006\006\034j\360\306+\357\274a\204\000\000;'
# -*- Python -*-
# Converted by ./convert_mime_type_table.py from:
# /usr/src2/apache_1.2b6/conf/mime.types
#
content_type_map = \
{
'ai': 'application/postscript',
'aif': 'audio/x-aiff',
'aifc': 'audio/x-aiff',
'aiff': 'audio/x-aiff',
'au': 'audio/basic',
'avi': 'video/x-msvideo',
'bcpio': 'application/x-bcpio',
'bin': 'application/octet-stream',
'cdf': 'application/x-netcdf',
'class': 'application/octet-stream',
'cpio': 'application/x-cpio',
'cpt': 'application/mac-compactpro',
'csh': 'application/x-csh',
'dcr': 'application/x-director',
'dir': 'application/x-director',
'dms': 'application/octet-stream',
'doc': 'application/msword',
'dvi': 'application/x-dvi',
'dxr': 'application/x-director',
'eps': 'application/postscript',
'etx': 'text/x-setext',
'exe': 'application/octet-stream',
'gif': 'image/gif',
'gtar': 'application/x-gtar',
'gz': 'application/x-gzip',
'hdf': 'application/x-hdf',
'hqx': 'application/mac-binhex40',
'htm': 'text/html',
'html': 'text/html',
'ice': 'x-conference/x-cooltalk',
'ief': 'image/ief',
'jpe': 'image/jpeg',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'kar': 'audio/midi',
'latex': 'application/x-latex',
'lha': 'application/octet-stream',
'lzh': 'application/octet-stream',
'man': 'application/x-troff-man',
'me': 'application/x-troff-me',
'mid': 'audio/midi',
'midi': 'audio/midi',
'mif': 'application/x-mif',
'mov': 'video/quicktime',
'movie': 'video/x-sgi-movie',
'mp2': 'audio/mpeg',
'mpe': 'video/mpeg',
'mpeg': 'video/mpeg',
'mpg': 'video/mpeg',
'mpga': 'audio/mpeg',
'mp3': 'audio/mpeg',
'ms': 'application/x-troff-ms',
'nc': 'application/x-netcdf',
'oda': 'application/oda',
'pbm': 'image/x-portable-bitmap',
'pdb': 'chemical/x-pdb',
'pdf': 'application/pdf',
'pgm': 'image/x-portable-graymap',
'png': 'image/png',
'pnm': 'image/x-portable-anymap',
'ppm': 'image/x-portable-pixmap',
'ppt': 'application/powerpoint',
'ps': 'application/postscript',
'qt': 'video/quicktime',
'ra': 'audio/x-realaudio',
'ram': 'audio/x-pn-realaudio',
'ras': 'image/x-cmu-raster',
'rgb': 'image/x-rgb',
'roff': 'application/x-troff',
'rpm': 'audio/x-pn-realaudio-plugin',
'rtf': 'application/rtf',
'rtx': 'text/richtext',
'sgm': 'text/x-sgml',
'sgml': 'text/x-sgml',
'sh': 'application/x-sh',
'shar': 'application/x-shar',
'sit': 'application/x-stuffit',
'skd': 'application/x-koan',
'skm': 'application/x-koan',
'skp': 'application/x-koan',
'skt': 'application/x-koan',
'snd': 'audio/basic',
'src': 'application/x-wais-source',
'sv4cpio': 'application/x-sv4cpio',
'sv4crc': 'application/x-sv4crc',
't': 'application/x-troff',
'tar': 'application/x-tar',
'tcl': 'application/x-tcl',
'tex': 'application/x-tex',
'texi': 'application/x-texinfo',
'texinfo': 'application/x-texinfo',
'tif': 'image/tiff',
'tiff': 'image/tiff',
'tr': 'application/x-troff',
'tsv': 'text/tab-separated-values',
'txt': 'text/plain',
'ustar': 'application/x-ustar',
'vcd': 'application/x-cdlink',
'vrml': 'x-world/x-vrml',
'wav': 'audio/x-wav',
'wrl': 'x-world/x-vrml',
'xbm': 'image/x-xbitmap',
'xpm': 'image/x-xpixmap',
'xwd': 'image/x-xwindowdump',
'xyz': 'chemical/x-pdb',
'zip': 'application/zip',
}
# -*- Mode: Python; tab-width: 4 -*-
RCS_ID = '$Id: producers.py,v 1.1 1999/01/09 03:17:32 amos Exp $'
import string
"""
A collection of producers.
Each producer implements a particular feature: They can be combined
in various ways to get interesting and useful behaviors.
For example, you can feed dynamically-produced output into the compressing
producer, then wrap this with the 'chunked' transfer-encoding producer.
"""
class simple_producer:
"producer for a string"
def __init__ (self, data, buffer_size=1024):
self.data = data
self.buffer_size = buffer_size
def more (self):
if len (self.data) > self.buffer_size:
result = self.data[:self.buffer_size]
self.data = self.data[self.buffer_size:]
return result
else:
result = self.data
self.data = ''
return result
class scanning_producer:
"like simple_producer, but more efficient for large strings"
def __init__ (self, data, buffer_size=1024):
self.data = data
self.buffer_size = buffer_size
self.pos = 0
def more (self):
if self.pos < len(self.data):
lp = self.pos
rp = min (
len(self.data),
self.pos + self.buffer_size
)
result = self.data[lp:rp]
self.pos = self.pos + len(result)
return result
else:
return ''
class lines_producer:
"producer for a list of lines"
def __init__ (self, lines):
self.lines = lines
def ready (self):
return len(self.lines)
def more (self):
if self.lines:
chunk = self.lines[:50]
self.lines = self.lines[50:]
return string.join (chunk, '\r\n') + '\r\n'
else:
return ''
class file_producer:
"producer wrapper for file[-like] objects"
# match http_channel's outgoing buffer size
out_buffer_size = 1<<16
def __init__ (self, file):
self.done = 0
self.file = file
def more (self):
if self.done:
return ''
else:
data = self.file.read (self.out_buffer_size)
if not data:
self.file.close()
del self.file
self.done = 1
return ''
else:
return data
# A simple output producer. This one does not [yet] have
# the safety feature builtin to the monitor channel: runaway
# output will not be caught.
# don't try to print from within any of the methods
# of this object.
class output_producer:
"Acts like an output file; suitable for capturing sys.stdout"
def __init__ (self):
self.data = ''
def write (self, data):
lines = string.splitfields (data, '\n')
data = string.join (lines, '\r\n')
self.data = self.data + data
def writeline (self, line):
self.data = self.data + line + '\r\n'
def writelines (self, lines):
self.data = self.data + string.joinfields (
lines,
'\r\n'
) + '\r\n'
def ready (self):
return (len (self.data) > 0)
def flush (self):
pass
def softspace (self, *args):
pass
def more (self):
if self.data:
result = self.data[:512]
self.data = self.data[512:]
return result
else:
return ''
class composite_producer:
"combine a fifo of producers into one"
def __init__ (self, producers):
self.producers = producers
def more (self):
while len(self.producers):
p = self.producers.first()
d = p.more()
if d:
return d
else:
self.producers.pop()
else:
return ''
class globbing_producer:
"""
'glob' the output from a producer into a particular buffer size.
helps reduce the number of calls to send(). [this appears to
gain about 30% performance on requests to a single channel]
"""
def __init__ (self, producer, buffer_size=1<<16):
self.producer = producer
self.buffer = ''
self.buffer_size = buffer_size
def more (self):
while len(self.buffer) < self.buffer_size:
data = self.producer.more()
if data:
self.buffer = self.buffer + data
else:
break
r = self.buffer
self.buffer = ''
return r
class hooked_producer:
"""
A producer that will call <function> when it empties,.
with an argument of the number of bytes produced. Useful
for logging/instrumentation purposes.
"""
def __init__ (self, producer, function):
self.producer = producer
self.function = function
self.bytes = 0
def more (self):
if self.producer:
result = self.producer.more()
if not result:
self.producer = None
self.function (self.bytes)
else:
self.bytes = self.bytes + len(result)
return result
else:
return ''
# HTTP 1.1 emphasizes that an advertised Content-Length header MUST be
# correct. In the face of Strange Files, it is conceivable that
# reading a 'file' may produce an amount of data not matching that
# reported by os.stat() [text/binary mode issues, perhaps the file is
# being appended to, etc..] This makes the chunked encoding a True
# Blessing, and it really ought to be used even with normal files.
# How beautifully it blends with the concept of the producer.
class chunked_producer:
"""A producer that implements the 'chunked' transfer coding for HTTP/1.1.
Here is a sample usage:
request['Transfer-Encoding'] = 'chunked'
request.push (
producers.chunked_producer (your_producer)
)
request.done()
"""
def __init__ (self, producer, footers=None):
self.producer = producer
self.footers = footers
def more (self):
if self.producer:
data = self.producer.more()
if data:
return '%x\r\n%s\r\n' % (len(data), data)
else:
self.producer = None
if self.footers:
return string.join (
['0'] + self.footers,
'\r\n'
) + '\r\n\r\n'
else:
return '0\r\n\r\n'
else:
return ''
# Unfortunately this isn't very useful right now (Aug 97), because
# apparently the browsers don't do on-the-fly decompression. Which
# is sad, because this could _really_ speed things up, especially for
# low-bandwidth clients (i.e., most everyone).
try:
import zlib
except ImportError:
zlib = None
class compressed_producer:
"""
Compress another producer on-the-fly, using ZLIB
[Unfortunately, none of the current browsers seem to support this]
"""
# Note: It's not very efficient to have the server repeatedly
# compressing your outgoing files: compress them ahead of time, or
# use a compress-once-and-store scheme. However, if you have low
# bandwidth and low traffic, this may make more sense than
# maintaining your source files compressed.
#
# Can also be used for compressing dynamically-produced output.
def __init__ (self, producer, level=5):
self.producer = producer
self.compressor = zlib.compressobj (level)
def more (self):
if self.producer:
cdata = ''
# feed until we get some output
while not cdata:
data = self.producer.more()
if not data:
self.producer = None
return self.compressor.flush()
else:
cdata = self.compressor.compress (data)
return cdata
else:
return ''
class escaping_producer:
"A producer that escapes a sequence of characters"
" Common usage: escaping the CRLF.CRLF sequence in SMTP, NNTP, etc..."
def __init__ (self, producer, esc_from='\r\n.', esc_to='\r\n..'):
self.producer = producer
self.esc_from = esc_from
self.esc_to = esc_to
self.buffer = ''
from asynchat import find_prefix_at_end
self.find_prefix_at_end = find_prefix_at_end
def more (self):
esc_from = self.esc_from
esc_to = self.esc_to
buffer = self.buffer + self.producer.more()
if buffer:
buffer = string.replace (buffer, esc_from, esc_to)
i = self.find_prefix_at_end (buffer, esc_from)
if i:
# we found a prefix
self.buffer = buffer[-i:]
return buffer[:-i]
else:
# no prefix, return it all
self.buffer = ''
return buffer
else:
return buffer
# -*- Mode: Python; tab-width: 4 -*-
#
# Author: Sam Rushing <rushing@nightmare.com>
#
RCS_ID = '$Id: resolver.py,v 1.1 1999/01/09 03:17:32 amos Exp $'
# Fast, low-overhead asynchronous name resolver. uses 'pre-cooked'
# DNS requests, unpacks only as much as it needs of the reply.
# see rfc1035 for details
import string
VERSION = string.split(RCS_ID)[2]
# header
# 1 1 1 1 1 1
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | ID |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# |QR| Opcode |AA|TC|RD|RA| Z | RCODE |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | QDCOUNT |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | ANCOUNT |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | NSCOUNT |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | ARCOUNT |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# question
# 1 1 1 1 1 1
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | |
# / QNAME /
# / /
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | QTYPE |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | QCLASS |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# build a DNS address request, _quickly_
def fast_address_request (host, id=0):
return (
'%c%c' % (chr((id>>8)&0xff),chr(id&0xff))
+ '\001\000\000\001\000\000\000\000\000\000%s\000\000\001\000\001' % (
string.join (
map (
lambda part: '%c%s' % (chr(len(part)),part),
string.split (host, '.')
), ''
)
)
)
def fast_ptr_request (host, id=0):
return (
'%c%c' % (chr((id>>8)&0xff),chr(id&0xff))
+ '\001\000\000\001\000\000\000\000\000\000%s\000\000\014\000\001' % (
string.join (
map (
lambda part: '%c%s' % (chr(len(part)),part),
string.split (host, '.')
), ''
)
)
)
def unpack_name (r,pos):
n = []
while 1:
ll = ord(r[pos])
if (ll&0xc0):
# compression
pos = (ll&0x3f << 8) + (ord(r[pos+1]))
elif ll == 0:
break
else:
pos = pos + 1
n.append (r[pos:pos+ll])
pos = pos + ll
return string.join (n,'.')
def skip_name (r,pos):
s = pos
while 1:
ll = ord(r[pos])
if (ll&0xc0):
# compression
return pos + 2
elif ll == 0:
pos = pos + 1
break
else:
pos = pos + ll + 1
return pos
def unpack_ttl (r,pos):
return reduce (
lambda x,y: (x<<8)|y,
map (ord, r[pos:pos+4])
)
# resource record
# 1 1 1 1 1 1
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | |
# / /
# / NAME /
# | |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | TYPE |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | CLASS |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | TTL |
# | |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | RDLENGTH |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
# / RDATA /
# / /
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
def unpack_address_reply (r):
ancount = (ord(r[6])<<8) + (ord(r[7]))
# skip question, first name starts at 12,
# this is followed by QTYPE and QCLASS
pos = skip_name (r, 12) + 4
if ancount:
# we are looking very specifically for
# an answer with TYPE=A, CLASS=IN (\000\001\000\001)
for an in range(ancount):
pos = skip_name (r, pos)
if r[pos:pos+4] == '\000\001\000\001':
return (
unpack_ttl (r,pos+4),
'%d.%d.%d.%d' % tuple(map(ord,r[pos+10:pos+14]))
)
# skip over TYPE, CLASS, TTL, RDLENGTH, RDATA
pos = pos + 8
rdlength = (ord(r[pos])<<8) + (ord(r[pos+1]))
pos = pos + 2 + rdlength
return 0, None
else:
return 0, None
def unpack_ptr_reply (r):
ancount = (ord(r[6])<<8) + (ord(r[7]))
# skip question, first name starts at 12,
# this is followed by QTYPE and QCLASS
pos = skip_name (r, 12) + 4
if ancount:
# we are looking very specifically for
# an answer with TYPE=PTR, CLASS=IN (\000\014\000\001)
for an in range(ancount):
pos = skip_name (r, pos)
if r[pos:pos+4] == '\000\014\000\001':
return (
unpack_ttl (r,pos+4),
unpack_name (r, pos+10)
)
# skip over TYPE, CLASS, TTL, RDLENGTH, RDATA
pos = pos + 8
rdlength = (ord(r[pos])<<8) + (ord(r[pos+1]))
pos = pos + 2 + rdlength
return 0, None
else:
return 0, None
from counter import counter
import asyncore
import socket
import sys
# This is a UDP (datagram) resolver.
#
# It may be useful to implement a TCP resolver. This would presumably
# give us more reliable behavior when things get too busy. A TCP
# client would have to manage the connection carefully, since the
# server is allowed to close it at will (the RFC recommends closing
# after 2 minutes of idle time).
#
# Note also that the TCP client will have to prepend each request
# with a 2-byte length indicator (see rfc1035).
#
class resolver (asyncore.dispatcher):
id = counter()
def __init__ (self, server='127.0.0.1'):
asyncore.dispatcher.__init__ (self)
self.create_socket (socket.AF_INET, socket.SOCK_DGRAM)
self.server = server
self.request_map = {}
def writable (self):
return 0
def log (self, *args):
pass
def handle_close (self):
print 'closing!'
self.close()
def get_id (self):
return (self.id.as_long() % (1<<16))
def resolve (self, host, callback):
self.socket.sendto (
fast_address_request (host, self.get_id()),
(self.server, 53)
)
self.request_map [self.get_id()] = host, unpack_address_reply, callback
self.id.increment()
def resolve_ptr (self, host, callback):
ip = string.split (host, '.')
ip.reverse()
ip = string.join (ip, '.') + '.in-addr.arpa'
self.socket.sendto (
fast_ptr_request (ip, self.get_id()),
(self.server, 53)
)
self.request_map [self.get_id()] = host, unpack_ptr_reply, callback
self.id.increment()
def handle_read (self):
reply, whence = self.socket.recvfrom (512)
# for security reasons we may want to double-check
# that <whence> is the server we sent the request to.
id = (ord(reply[0])<<8) + ord(reply[1])
if self.request_map.has_key (id):
host, unpack, callback = self.request_map[id]
del self.request_map[id]
ttl, answer = unpack (reply)
try:
callback (host, ttl, answer)
except:
t,v,tb = sys.exc_info()
(file,fun,line), tbinfo = asyncore.compact_traceback (t,v,tb)
print t,v
print tbinfo
class rbl (resolver):
def resolve_maps (self, host, callback):
ip = string.split (host, '.')
ip.reverse()
ip = string.join (ip, '.') + '.rbl.maps.vix.com'
self.socket.sendto (
fast_ptr_request (ip, self.get_id()),
(self.server, 53)
)
self.request_map [self.get_id()] = host, self.check_reply, callback
self.id.increment()
def check_reply (self, r):
# we only need to check RCODE.
rcode = (ord(r[3])&0xf)
print 'MAPS RBL; RCODE =%02x' % rcode
print repr(r)
return 0, rcode # (ttl, answer)
import time
class hooked_callback:
def __init__ (self, hook, callback):
self.hook, self.callback = hook, callback
def __call__ (self, *args):
apply (self.hook, args)
apply (self.callback, args)
class caching_resolver (resolver):
"Cache DNS queries. Will need to honor the TTL value in the replies"
def __init__ (*args):
apply (resolver.__init__, args)
self = args[0]
self.cache = {}
self.forward_requests = counter()
self.reverse_requests = counter()
self.cache_hits = counter()
def resolve (self, host, callback):
self.forward_requests.increment()
if self.cache.has_key (host):
when, ttl, answer = self.cache[host]
# ignore TTL for now
callback (host, ttl, answer)
self.cache_hits.increment()
else:
resolver.resolve (
self,
host,
hooked_callback (
self.callback_hook,
callback
)
)
def resolve_ptr (self, host, callback):
self.reverse_requests.increment()
if self.cache.has_key (host):
when, ttl, answer = self.cache[host]
# ignore TTL for now
callback (host, ttl, answer)
self.cache_hits.increment()
else:
resolver.resolve_ptr (
self,
host,
hooked_callback (
self.callback_hook,
callback
)
)
def callback_hook (self, host, ttl, answer):
self.cache[host] = time.time(), ttl, answer
SERVER_IDENT = 'Caching DNS Resolver (V%s)' % VERSION
def status (self):
import status_handler
import producers
return producers.simple_producer (
'<h2>%s</h2>' % self.SERVER_IDENT
+ '<br>Server: %s' % self.server
+ '<br>Cache Entries: %d' % len(self.cache)
+ '<br>Outstanding Requests: %d' % len(self.request_map)
+ '<br>Forward Requests: %s' % self.forward_requests
+ '<br>Reverse Requests: %s' % self.reverse_requests
+ '<br>Cache Hits: %s' % self.cache_hits
)
#test_reply = """\000\000\205\200\000\001\000\001\000\002\000\002\006squirl\011nightmare\003com\000\000\001\000\001\300\014\000\001\000\001\000\001Q\200\000\004\315\240\260\005\011nightmare\003com\000\000\002\000\001\000\001Q\200\000\002\300\014\3006\000\002\000\001\000\001Q\200\000\015\003ns1\003iag\003net\000\300\014\000\001\000\001\000\001Q\200\000\004\315\240\260\005\300]\000\001\000\001\000\000\350\227\000\004\314\033\322\005"""
# def test_unpacker ():
# print unpack_address_reply (test_reply)
#
# import time
# class timer:
# def __init__ (self):
# self.start = time.time()
# def end (self):
# return time.time() - self.start
#
# # I get ~290 unpacks per second for the typical case, compared to ~48
# # using dnslib directly. also, that latter number does not include
# # picking the actual data out.
#
# def benchmark_unpacker():
#
# r = range(1000)
# t = timer()
# for i in r:
# unpack_address_reply (test_reply)
# print '%.2f unpacks per second' % (1000.0 / t.end())
if __name__ == '__main__':
import sys
if len(sys.argv) == 1:
print 'usage: %s [-r] [-s <server_IP>] host [host ...]' % sys.argv[0]
sys.exit(0)
elif ('-s' in sys.argv):
i = sys.argv.index('-s')
server = sys.argv[i+1]
del sys.argv[i:i+2]
else:
server = '127.0.0.1'
if ('-r' in sys.argv):
reverse = 1
i = sys.argv.index('-r')
del sys.argv[i]
else:
reverse = 0
if ('-m' in sys.argv):
maps = 1
sys.argv.remove ('-m')
else:
maps = 0
if maps:
r = rbl (server)
else:
r = caching_resolver(server)
count = len(sys.argv) - 1
def print_it (host, ttl, answer):
global count
print '%s: %s' % (host, answer)
count = count - 1
if not count:
r.close()
for host in sys.argv[1:]:
if reverse:
r.resolve_ptr (host, print_it)
elif maps:
r.resolve_maps (host, print_it)
else:
r.resolve (host, print_it)
# hooked asyncore.loop()
while asyncore.socket_map:
asyncore.poll (30.0)
print 'requests outstanding: %d' % len(r.request_map)
# -*- Mode: Python; tab-width: 4 -*-
#
# medusa status extension
#
import string
import time
import regex
import asyncore
import http_server
import medusa_gif
import producers
from counter import counter
START_TIME = long(time.time())
# split a uri
# <path>;<params>?<query>#<fragment>
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))
class status_extension:
hit_counter = counter()
hyper_regex = regex.compile ('/status/object/\([0-9]+\)/.*')
def __init__ (self, objects, regexp='/status\(/.*\)?'):
self.objects = objects
self.regexp = regex.compile (regexp)
self.hyper_objects = []
for object in objects:
self.register_hyper_object (object)
def __repr__ (self):
return '<Status Extension (%s hits) at %x>' % (
self.hit_counter,
id(self)
)
def match (self, request):
[path, params, query, fragment] = split_path (request.uri)
return self.regexp.match (path) == len(path)
# Possible Targets:
# /status
# /status/channel_list
# /status/medusa.gif
# can we have 'clickable' objects?
# [yes, we can use id(x) and do a linear search]
# Dynamic producers:
# HTTP/1.0: we must close the channel, because it's dynamic output
# HTTP/1.1: we can use the chunked transfer-encoding, and leave
# it open.
def handle_request (self, request):
[path, params, query, fragment] = split_path (request.uri)
self.hit_counter.increment()
if path == '/status':
up_time = string.join (english_time (long(time.time()) - START_TIME))
request['Content-Type'] = 'text/html'
request.push (
'<html>'
'<title>Medusa Status Reports</title>'
'<body bgcolor="#ffffff">'
'<h1>Medusa Status Reports</h1>'
'<b>Up:</b> %s' % up_time
)
for i in range(len(self.objects)):
request.push (self.objects[i].status())
request.push ('<hr>\r\n')
request.push (
'<p><a href="/status/channel_list">Channel List</a>'
'<hr>'
'<img src="/status/medusa.gif" align=right width=%d height=%d>' % (
medusa_gif.width,
medusa_gif.height
) +
'</body></html>'
)
request.done()
elif path == '/status/channel_list':
request['Content-Type'] = 'text/html'
request.push ('<html><body>')
request.push(channel_list_producer())
request.push (
'<hr>'
'<img src="/status/medusa.gif" align=right width=%d height=%d>' % (
medusa_gif.width,
medusa_gif.height
) +
'</body></html>'
)
request.done()
elif path == '/status/medusa.gif':
request['Content-Type'] = 'image/gif'
request['Content-Length'] = len(medusa_gif.data)
request.push (medusa_gif.data)
request.done()
elif path == '/status/close_zombies':
message = (
'<h2>Closing all zombie http client connections...</h2>'
'<p><a href="/status">Back to the status page</a>'
)
request['Content-Type'] = 'text/html'
request['Content-Length'] = len (message)
request.push (message)
now = int (time.time())
for channel in asyncore.socket_map.keys():
if channel.__class__ == http_server.http_channel:
if channel != request.channel:
if (now - channel.creation_time) > channel.zombie_timeout:
channel.close()
request.done()
# Emergency Debug Mode
# If a server is running away from you, don't KILL it!
# Move all the AF_INET server ports and perform an autopsy...
# [disabled by default to protect the innocent]
#elif path == '/status/emergency_debug':
elif 0:
request.push ('<html>Moving All Servers...</html>')
request.done()
for channel in asyncore.socket_map.keys():
if channel.accepting:
if type(channel.addr) is type(()):
ip, port = channel.addr
channel.socket.close()
channel.del_channel()
channel.addr = (ip, port+10000)
fam, typ = channel.family_and_type
channel.create_socket (fam, typ)
channel.set_reuse_addr()
channel.bind (channel.addr)
channel.listen(5)
elif self.hyper_regex.match (path) != -1:
oid = string.atoi (self.hyper_regex.group (1))
for object in self.hyper_objects:
if id (object) == oid:
if hasattr (object, 'hyper_respond'):
object.hyper_respond (self, path, request)
else:
request.error (404)
return
def status (self):
return producers.simple_producer (
'<li>Status Extension <b>Hits</b> : %s' % self.hit_counter
)
def register_hyper_object (self, object):
if not object in self.hyper_objects:
self.hyper_objects.append (object)
import logger
class logger_for_status (logger.tail_logger):
def status (self):
return 'Last %d log entries for: %s' % (
len (self.messages),
html_repr (self)
)
def hyper_respond (self, sh, path, request):
request['Content-Type'] = 'text/plain'
messages = self.messages[:]
messages.reverse()
request.push (lines_producer (messages))
request.done()
class lines_producer:
def __init__ (self, lines):
self.lines = lines
def ready (self):
return len(self.lines)
def more (self):
if self.lines:
chunk = self.lines[:50]
self.lines = self.lines[50:]
return string.join (chunk, '\r\n') + '\r\n'
else:
return ''
class channel_list_producer (lines_producer):
def __init__ (self):
channel_reprs = map (
lambda x: '&lt;' + repr(x)[1:-1] + '&gt;',
asyncore.socket_map.keys()
)
channel_reprs.sort()
lines_producer.__init__ (
self,
['<h1>Active Channel List</h1>',
'<pre>'
] + channel_reprs + [
'</pre>',
'<p><a href="/status">Status Report</a>'
]
)
# this really needs a full-blown quoter...
def sanitize (s):
if '<' in s:
s = string.join (string.split (s, '<'), '&lt;')
if '>' in s:
s = string.join (string.split (s, '>'), '&gt;')
return s
def html_repr (object):
so = sanitize (repr (object))
if hasattr (object, 'hyper_respond'):
return '<a href="/status/object/%d/">%s</a>' % (id (object), so)
else:
return so
def html_reprs (list, front='', back=''):
reprs = map (
lambda x,f=front,b=back: '%s%s%s' % (f,x,b),
map (lambda x: sanitize (html_repr(x)), list)
)
reprs.sort()
return reprs
# for example, tera, giga, mega, kilo
# p_d (n, (1024, 1024, 1024, 1024))
# smallest divider goes first - for example
# minutes, hours, days
# p_d (n, (60, 60, 24))
def progressive_divide (n, parts):
result = []
for part in parts:
n, rem = divmod (n, part)
result.append (rem)
result.append (n)
return result
# b,k,m,g,t
def split_by_units (n, units, dividers, format_string):
divs = progressive_divide (n, dividers)
result = []
for i in range(len(units)):
if divs[i]:
result.append (format_string % (divs[i], units[i]))
result.reverse()
if not result:
return [format_string % (0, units[0])]
else:
return result
def english_bytes (n):
return split_by_units (
n,
('','K','M','G','T'),
(1024, 1024, 1024, 1024, 1024),
'%d %sB'
)
def english_time (n):
return split_by_units (
n,
('secs', 'mins', 'hours', 'days', 'weeks', 'years'),
( 60, 60, 24, 7, 52),
'%d %s'
)
# -*- Mode: Python; tab-width: 4 -*-
# It is tempting to add an __int__ method to this class, but it's not
# a good idea. This class tries to gracefully handle integer
# overflow, and to hide this detail from both the programmer and the
# user. Note that the __str__ method can be relied on for printing out
# the value of a counter:
#
# >>> print 'Total Client: %s' % self.total_clients
#
# If you need to do arithmetic with the value, then use the 'as_long'
# method, the use of long arithmetic is a reminder that the counter
# will overflow.
class counter:
"general-purpose counter"
def __init__ (self, initial_value=0):
self.value = initial_value
def increment (self, delta=1):
result = self.value
try:
self.value = self.value + delta
except OverflowError:
self.value = long(self.value) + delta
return result
def decrement (self, delta=1):
result = self.value
try:
self.value = self.value - delta
except OverflowError:
self.value = long(self.value) - delta
return result
def as_long (self):
return long(self.value)
def __nonzero__ (self):
return self.value != 0
def __repr__ (self):
return '<counter value=%s at %x>' % (self.value, id(self))
def __str__ (self):
return str(long(self.value))[:-1]
# -*- Mode: Python; tab-width: 4 -*-
#
# Author: Sam Rushing <rushing@nightmare.com>
# Copyright 1997 by Sam Rushing
# All Rights Reserved.
#
# This software is provided free for non-commercial use. If you are
# interested in using this software in a commercial context, or in
# purchasing support, please contact the author.
RCS_ID = '$Id: default_handler.py,v 1.1 1999/01/09 03:17:31 amos 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 <lib/urllib.py>
_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>;<params>?<query>#<fragment>
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 <default_with_post_handler>, 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 = 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 (
'<li>%s' % status_handler.html_repr (self)
+ '<ul>'
+ ' <li><b>Total Hits:</b> %s' % self.hit_counter
+ ' <li><b>Files Delivered:</b> %s' % self.file_counter
+ ' <li><b>Cache Hits:</b> %s' % self.cache_counter
+ '</ul>'
)
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 ''
# -*- Mode: Python; tab-width: 4 -*-
import regex
import string
import time
def concat (*args):
return string.joinfields (args, '')
def join (seq, field=' '):
return string.joinfields (seq, field)
def group (s):
return '\\(' + s + '\\)'
short_days = ['sun','mon','tue','wed','thu','fri','sat']
long_days = ['sunday','monday','tuesday','wednesday','thursday','friday','saturday']
short_day_reg = group (join (short_days, '\\|'))
long_day_reg = group (join (long_days, '\\|'))
daymap = {}
for i in range(7):
daymap[short_days[i]] = i
daymap[long_days[i]] = i
hms_reg = join (3 * [group('[0-9][0-9]')], ':')
months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec']
monmap = {}
for i in range(12):
monmap[months[i]] = i+1
months_reg = group (join (months, '\\|'))
# From draft-ietf-http-v11-spec-07.txt/3.3.1
# Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123
# Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
# Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
# rfc822 format
rfc822_date = join (
[concat (short_day_reg,','), # day
group('[0-9][0-9]?'), # date
months_reg, # month
group('[0-9]+'), # year
hms_reg, # hour minute second
'gmt'
],
' '
)
rfc822_reg = regex.compile (rfc822_date)
def unpack_rfc822 ():
g = rfc822_reg.group
a = string.atoi
return (
a(g(4)), # year
monmap[g(3)], # month
a(g(2)), # day
a(g(5)), # hour
a(g(6)), # minute
a(g(7)), # second
0,
0,
0
)
# rfc850 format
rfc850_date = join (
[concat (long_day_reg,','),
join (
[group ('[0-9][0-9]?'),
months_reg,
group ('[0-9]+')
],
'-'
),
hms_reg,
'gmt'
],
' '
)
rfc850_reg = regex.compile (rfc850_date)
# they actually unpack the same way
def unpack_rfc850 ():
g = rfc850_reg.group
a = string.atoi
return (
a(g(4)), # year
monmap[g(3)], # month
a(g(2)), # day
a(g(5)), # hour
a(g(6)), # minute
a(g(7)), # second
0,
0,
0
)
# parsdate.parsedate - ~700/sec.
# parse_http_date - ~1333/sec.
def build_http_date (when):
return time.strftime ('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(when))
def parse_http_date (d):
d = string.lower (d)
# Thanks to Craig Silverstein <csilvers@google.com> for pointing
# out the DST discrepancy
if time.daylight:
tz = time.altzone
else:
tz = time.timezone
# rfc850 comes first, netscape uses it. <frown>
if rfc850_reg.match (d) == len(d):
return int (time.mktime (unpack_rfc850()) - tz)
elif rfc822_reg.match (d) == len(d):
return int (time.mktime (unpack_rfc822()) - tz)
else:
return 0
#! /usr/local/bin/python
# -*- Mode: Python; tab-width: 4 -*-
#
# Author: Sam Rushing <rushing@nightmare.com>
# Copyright 1996, 1997 by Sam Rushing
# All Rights Reserved.
#
# This software is provided free for non-commercial use. If you are
# interested in using this software in a commercial context, or in
# purchasing support, please contact the author.
RCS_ID = '$Id: http_server.py,v 1.1 1999/01/09 03:17:32 amos Exp $'
# python modules
import os
import regex
import socket
import stat
import string
import sys
import time
# async modules
import asyncore
import asynchat
# medusa modules
import http_date
import producers
import status_handler
import logger
VERSION_STRING = string.split(RCS_ID)[2]
from counter import counter
# ===========================================================================
# Request Object
# ===========================================================================
class http_request:
# default reply code
reply_code = 200
request_counter = counter()
# Whether to automatically use chunked encoding when
#
# HTTP version is 1.1
# Content-Length is not set
# Chunked encoding is not already in effect
#
# If your clients are having trouble, you might want to disable this.
use_chunked = 1
# by default, this request object ignores user data.
collector = None
def __init__ (self, *args):
# unpack information about the request
(self.channel, self.request,
self.command, self.uri, self.version,
self.header) = args
self.outgoing = fifo()
self.reply_headers = {
'Server' : 'Medusa/%s' % VERSION_STRING,
'Date' : http_date.build_http_date (time.time())
}
self.request_number = http_request.request_counter.increment()
# --------------------------------------------------
# reply header management
# --------------------------------------------------
def __setitem__ (self, key, value):
self.reply_headers[key] = value
def __getitem__ (self, key):
return self.reply_headers[key]
def has_key (self, key):
return self.reply_headers.has_key (key)
def build_reply_header (self):
return string.join (
[self.response(self.reply_code)] + map (
lambda x: '%s: %s' % x,
self.reply_headers.items()
),
'\r\n'
) + '\r\n\r\n'
# --------------------------------------------------
# user data
# --------------------------------------------------
def collect_incoming_data (self, data):
if self.collector:
self.collector.collect_incoming_data (data)
else:
sys.stderr.write (
'warning: dropping %d bytes of incoming request data\n' % len(data)
)
def found_terminator (self):
if self.collector:
self.collector.collect_incoming_data (data)
else:
sys.stderr.write (
'warning: unexpected end-of-record for incoming request\n'
)
def push (self, producer):
if type(producer) == type(''):
self.outgoing.push (producers.simple_producer (producer))
else:
self.outgoing.push (producer)
def response (self, code=200):
message = self.responses[code]
self.reply_code = code
return 'HTTP/%s %d %s' % (self.version, code, message)
def error (self, code):
self.reply_code = code
message = self.responses[code]
s = self.DEFAULT_ERROR_MESSAGE % {
'code': code,
'message': message,
}
self['Content-Length'] = len(s)
self['Content-Type'] = 'text/html'
# make an error reply
self.push (s)
self.done()
# can also be used for empty replies
reply_now = error
def done (self):
"finalize this transaction - send output to the http channel"
# ----------------------------------------
# persistent connection management
# ----------------------------------------
# --- BUCKLE UP! ----
connection = string.lower (get_header (CONNECTION, self.header))
close_it = 0
wrap_in_chunking = 0
if self.version == '1.0':
if connection == 'keep-alive':
if not self.has_key ('Content-Length'):
close_it = 1
else:
self['Connection'] = 'Keep-Alive'
else:
close_it = 1
elif self.version == '1.1':
if connection == 'close':
close_it = 1
elif not self.has_key ('Content-Length'):
if self.has_key ('Transfer-Encoding'):
if not self['Transfer-Encoding'] == 'chunked':
close_it = 1
elif self.use_chunked:
self['Transfer-Encoding'] = 'chunked'
wrap_in_chunking = 1
else:
close_it = 1
outgoing_header = producers.simple_producer (self.build_reply_header())
if close_it:
self['Connection'] = 'close'
if wrap_in_chunking:
outgoing_producer = producers.chunked_producer (
producers.composite_producer (self.outgoing)
)
# prepend the header
outgoing_producer = producers.composite_producer (
fifo([outgoing_header, outgoing_producer])
)
else:
# prepend the header
self.outgoing.push_front (outgoing_header)
outgoing_producer = producers.composite_producer (self.outgoing)
# apply a few final transformations to the output
self.channel.push_with_producer (
# globbing gives us large packets
producers.globbing_producer (
# hooking lets us log the number of bytes sent
producers.hooked_producer (
outgoing_producer,
self.log
)
)
)
self.channel.current_request = None
if close_it:
self.channel.close_when_done()
def log (self, bytes):
print 'request %3d: %s %d bytes' % (
self.request_counter,
self.request,
bytes
)
def log_date_string (self, when):
return time.strftime (
'%d/%b/%Y:%H:%M:%S ',
time.gmtime(when)
) + tz_for_log
def log (self, bytes):
self.channel.server.logger.log (
self.channel.addr[0],
'%d - - [%s] "%s" %d %d\n' % (
self.channel.addr[1],
self.log_date_string (time.time()),
self.request,
self.reply_code,
bytes
)
)
responses = {
100: "Continue",
101: "Switching Protocols",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Moved Temporarily",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Time-out",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Request Entity Too Large",
414: "Request-URI Too Large",
415: "Unsupported Media Type",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Time-out",
505: "HTTP Version not supported"
}
# Default error message
DEFAULT_ERROR_MESSAGE = string.join (
['<head>',
'<title>Error response</title>',
'</head>',
'<body>',
'<h1>Error response</h1>',
'<p>Error code %(code)d.',
'<p>Message: %(message)s.',
'</body>',
''
],
'\r\n'
)
# ===========================================================================
# HTTP Channel Object
# ===========================================================================
class http_channel (asynchat.async_chat):
# use a larger default output buffer
ac_out_buffer_size = 1<<16
current_request = None
channel_counter = counter()
def __init__ (self, server, conn, addr):
self.channel_number = http_channel.channel_counter.increment()
self.request_counter = counter()
asynchat.async_chat.__init__ (self, conn)
self.server = server
self.addr = addr
self.set_terminator ('\r\n\r\n')
self.in_buffer = ''
self.creation_time = int (time.time())
self.check_maintenance()
def __repr__ (self):
ar = asynchat.async_chat.__repr__(self)[1:-1]
return '<%s channel#: %s requests:%s>' % (
ar,
self.channel_number,
self.request_counter
)
# Channel Counter, Maintenance Interval...
maintenance_interval = 500
def check_maintenance (self):
if not self.channel_number % self.maintenance_interval:
self.maintenance()
def maintenance (self):
self.kill_zombies()
# 30-minute zombie timeout. status_handler also knows how to kill zombies.
zombie_timeout = 30 * 60
def kill_zombies (self):
now = int (time.time())
for channel in asyncore.socket_map.keys():
if channel.__class__ == http_channel:
if (now - channel.creation_time) > channel.zombie_timeout:
channel.close()
# --------------------------------------------------
# send/recv overrides, good place for instrumentation.
# --------------------------------------------------
# this information needs to get into the request object,
# so that it may log correctly.
def send (self, data):
result = asynchat.async_chat.send (self, data)
self.server.bytes_out.increment (len(data))
return result
def recv (self, buffer_size):
result = asynchat.async_chat.recv (self, buffer_size)
self.server.bytes_in.increment (len(result))
return result
def log (self, *args):
pass
# --------------------------------------------------
# async_chat methods
# --------------------------------------------------
def collect_incoming_data (self, data):
if self.current_request:
# we are receiving data (probably POST data) for a request
self.current_request.collect_incoming_data (data)
else:
# we are receiving header (request) data
self.in_buffer = self.in_buffer + data
def found_terminator (self):
if self.current_request:
self.current_request.found_terminator()
else:
header = self.in_buffer
self.in_buffer = ''
lines = string.split (header, '\r\n')
# --------------------------------------------------
# crack the request header
# --------------------------------------------------
request = lines[0]
command, uri, version = crack_request (request)
header = join_headers (lines[1:])
r = http_request (self, request, command, uri, version, header)
self.request_counter.increment()
self.server.total_requests.increment()
# --------------------------------------------------
# handler selection and dispatch
# --------------------------------------------------
for h in self.server.handlers:
if h.match (r):
try:
self.current_request = r
# This isn't used anywhere.
# r.handler = h # CYCLE
h.handle_request (r)
except:
self.server.exceptions.increment()
t,v,tb = sys.exc_info()
(file,fun,line),tbinfo = asyncore.compact_traceback (t,v,tb)
while tb.tb_next:
tb = tb.tb_next
# Log this to a better place.
print 'Server Error: %s, %s: file: %s line: %s' % (t,v,file,line)
# IMPORTANT: without this <del>, this entire connection will leak. [why?]
del t,v,tb
try:
r.error (500)
except:
pass
return
# no handlers, so complain
r.error (404)
def writable (self):
# this is just the normal async_chat 'writable', here for comparison
return self.ac_out_buffer or len(self.producer_fifo)
def writable_for_proxy (self):
# this version of writable supports the idea of a 'stalled' producer
# [i.e., it's not ready to produce any output yet] This is needed by
# the proxy, which will be waiting for the magic combination of
# 1) hostname resolved
# 2) connection made
# 3) data available.
if self.ac_out_buffer:
return 1
elif len(self.producer_fifo):
p = self.producer_fifo.first()
if hasattr (p, 'stalled'):
return not p.stalled()
else:
return 1
# ===========================================================================
# HTTP Server Object
# ===========================================================================
class http_server (asyncore.dispatcher):
SERVER_IDENT = 'HTTP Server (V%s)' % VERSION_STRING
channel_class = http_channel
def __init__ (self, ip, port, resolver=None, logger_object=None):
self.ip = ip
self.port = port
asyncore.dispatcher.__init__ (self)
self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
self.handlers = []
if not logger_object:
logger_object = logger.file_logger (sys.stdout)
self.set_reuse_addr()
self.bind ((ip, port))
self.listen (5)
host, port = self.socket.getsockname()
if not ip:
print 'Warning: computing default hostname'
self.server_name = socket.gethostbyaddr (
socket.gethostbyname (socket.gethostname())
)[0]
else:
self.server_name = socket.gethostbyaddr (ip)[0]
self.server_port = port
self.total_clients = counter()
self.total_requests = counter()
self.exceptions = counter()
self.bytes_out = counter()
self.bytes_in = counter()
if not logger_object:
logger_object = logger.file_logger (sys.stdout)
if resolver:
self.logger = logger.resolving_logger (resolver, logger_object)
else:
self.logger = logger.unresolving_logger (logger_object)
sys.stdout.write (
'Medusa (V%s) started at %s'
'\n\tHostname: %s'
'\n\tPort:%d'
'\n' % (
VERSION_STRING,
time.ctime(time.time()),
self.server_name,
port,
)
)
def writable (self):
return 0
def handle_read (self):
pass
def readable (self):
return self.accepting
def handle_connect (self):
pass
def handle_accept (self):
self.total_clients.increment()
try:
conn, addr = self.accept()
except socket.error:
# linux: on rare occasions we get a bogus socket back from
# accept. socketmodule.c:makesockaddr complains that the
# address family is unknown. We don't want the whole server
# to shut down because of this.
sys.stderr.write ('warning: server accept() threw an exception\n')
return
self.channel_class (self, conn, addr)
def install_handler (self, handler, back=0):
if back:
self.handlers.append (handler)
else:
self.handlers.insert (0, handler)
def remove_handler (self, handler):
self.handlers.remove (handler)
def status (self):
def nice_bytes (n):
return string.join (status_handler.english_bytes (n))
handler_stats = filter (None, map (maybe_status, self.handlers))
if self.total_clients:
ratio = float(self.total_requests.as_long()/self.total_clients.as_long())
else:
ratio = 0.0
return producers.composite_producer (
fifo ([producers.lines_producer (
['<h2>%s</h2>' % self.SERVER_IDENT,
'<br>Listening on: <b>Host:</b> %s' % self.server_name,
'<b>Port:</b> %d' % self.port,
'<p><ul>'
'<li>Total <b>Clients:</b> %s' % self.total_clients,
'<b>Requests:</b> %s' % self.total_requests,
'<b>Requests/Client:</b> %.1f' % (ratio),
'<li>Total <b>Bytes In:</b> %s' % (nice_bytes (self.bytes_in.as_long())),
'<b>Bytes Out:</b> %s' % (nice_bytes (self.bytes_out.as_long())),
'<li>Total <b>Exceptions:</b> %s' % self.exceptions,
'</ul><p>'
'<b>Extension List</b><ul>',
])] + handler_stats + [producers.simple_producer('</ul>')]
)
)
def maybe_status (thing):
if hasattr (thing, 'status'):
return thing.status()
else:
return None
CONNECTION = regex.compile ('Connection: \(.*\)', regex.casefold)
# merge multi-line headers
# [486dx2: ~500/sec]
def join_headers (headers):
r = []
for i in range(len(headers)):
if headers[i][0] in ' \t':
r[-1] = r[-1] + headers[i][1:]
else:
r.append (headers[i])
return r
def get_header (head_reg, lines, group=1):
for line in lines:
if head_reg.match (line) == len(line):
return head_reg.group(group)
return ''
REQUEST = regex.compile ('\([^ ]+\) \([^ ]+\)\(\( HTTP/\([0-9.]+\)\)$\|$\)')
def crack_request (r):
if REQUEST.match (r) == len(r):
if REQUEST.group(3):
version = REQUEST.group(5)
else:
version = None
return string.lower (REQUEST.group (1)), REQUEST.group(2), version
class fifo:
def __init__ (self, list=None):
if not list:
self.list = []
else:
self.list = list
def __len__ (self):
return len(self.list)
def first (self):
return self.list[0]
def push_front (self, object):
self.list.insert (0, object)
def push (self, data):
self.list.append (data)
def pop (self):
if self.list:
result = self.list[0]
del self.list[0]
return (1, result)
else:
return (0, None)
def compute_timezone_for_log ():
if time.daylight:
tz = time.altzone
else:
tz = time.timezone
if tz > 0:
neg = 1
else:
neg = 0
tz = -tz
h, rem = divmod (tz, 3600)
m, rem = divmod (rem, 60)
if neg:
return '-%02d%02d' % (h, m)
else:
return '+%02d%02d' % (h, m)
# if you run this program over a TZ change boundary, this will be invalid.
tz_for_log = compute_timezone_for_log()
if __name__ == '__main__':
import sys
if len(sys.argv) < 2:
print 'usage: %s <root> <port>' % (sys.argv[0])
else:
import monitor
import filesys
import default_handler
import status_handler
import ftp_server
import chat_server
import resolver
import logger
rs = resolver.caching_resolver ('127.0.0.1')
lg = logger.file_logger (sys.stdout)
ms = monitor.secure_monitor_server ('fnord', '127.0.0.1', 9999)
fs = filesys.os_filesystem (sys.argv[1])
dh = default_handler.default_handler (fs)
hs = http_server ('', string.atoi (sys.argv[2]), rs, lg)
hs.install_handler (dh)
ftp = ftp_server.ftp_server (
ftp_server.dummy_authorizer(sys.argv[1]),
port=8021,
resolver=rs,
logger_object=lg
)
cs = chat_server.chat_server ('', 7777)
sh = status_handler.status_extension([hs,ms,ftp,cs,rs])
hs.install_handler (sh)
if ('-p' in sys.argv):
def profile_loop ():
try:
asyncore.loop()
except KeyboardInterrupt:
pass
import profile
profile.run ('profile_loop()', 'profile.out')
else:
asyncore.loop()
# -*- Mode: Python; tab-width: 4 -*-
import asynchat
import socket
import string
#
# three types of log:
# 1) file
# with optional flushing.
# 2) socket
# dump output directly to a socket connection. [how do we
# keep it open?]
# 3) syslog
# log to syslog via tcp. this is a per-line protocol.
#
#
# The 'standard' interface to a logging object is simply
# log_object.log (message)
#
# a file-like object that captures output, and
# makes sure to flush it always... this could
# be connected to:
# o stdio file
# o low-level file
# o socket channel
# o syslog output...
class file_logger:
# pass this either a path or a file object.
def __init__ (self, file, flush=1, mode='wa'):
if type(file) == type(''):
if (file == '-'):
import sys
self.file = sys.stdout
else:
self.file = open (file, mode)
else:
self.file = file
self.do_flush = flush
def __repr__ (self):
return '<file logger: %s>' % self.file
def write (self, data):
self.file.write (data)
self.maybe_flush()
def writeline (self, line):
self.file.writeline (line)
self.maybe_flush()
def writelines (self, lines):
self.file.writelines (lines)
self.maybe_flush()
def maybe_flush (self):
if self.do_flush:
self.file.flush()
def flush (self):
self.file.flush()
def softspace (self, *args):
pass
def log (self, message):
if message[-1] not in ('\r', '\n'):
self.write (message + '\n')
else:
self.write (message)
# syslog is a line-oriented log protocol - this class would be
# appropriate for FTP or HTTP logs, but not for dumping stderr to.
# TODO: a simple safety wrapper that will ensure that the line sent
# to syslog is reasonable.
# TODO: async version of syslog_client: now, log entries use blocking
# send()
import m_syslog
syslog_logger = m_syslog.syslog_client
class syslog_logger (m_syslog.syslog_client):
def __init__ (self, address, facility='user'):
m_syslog.syslog_client.__init__ (self, address)
self.facility = m_syslog.facility_names[facility]
self.address=address
def __repr__ (self):
return '<syslog logger address=%s>' % (repr(self.address))
def log (self, message):
m_syslog.syslog_client.log (
self,
message,
facility=self.facility,
priority=m_syslog.LOG_INFO
)
# log to a stream socket, asynchronously
class socket_logger (asynchat.async_chat):
def __init__ (self, address):
if type(address) == type(''):
self.create_socket (socket.AF_UNIX, socket.SOCK_STREAM)
else:
self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
self.connect (address)
self.address = address
def __repr__ (self):
return '<socket logger: address=%s>' % (self.address)
def log (self, message):
if message[-2:] != '\r\n':
self.socket.push (message + '\r\n')
else:
self.socket.push (message)
# log to multiple places
class multi_logger:
def __init__ (self, loggers):
self.loggers = loggers
def __repr__ (self):
return '<multi logger: %s>' % (repr(self.loggers))
def log (self, message):
for logger in self.loggers:
logger.log (message)
class resolving_logger:
"""Feed (ip, message) combinations into this logger to get a
resolved hostname in front of the message. The message will not
be logged until the PTR request finishes (or fails)."""
def __init__ (self, resolver, logger):
self.resolver = resolver
self.logger = logger
class logger_thunk:
def __init__ (self, message, logger):
self.message = message
self.logger = logger
def __call__ (self, host, ttl, answer):
if not answer:
answer = host
self.logger.log ('%s:%s' % (answer, self.message))
def log (self, ip, message):
self.resolver.resolve_ptr (
ip,
self.logger_thunk (
message,
self.logger
)
)
class unresolving_logger:
"Just in case you don't want to resolve"
def __init__ (self, logger):
self.logger = logger
def log (self, ip, message):
self.logger.log ('%s:%s' % (ip, message))
def strip_eol (line):
while line and line[-1] in '\r\n':
line = line[:-1]
return line
class tail_logger:
"Keep track of the last <size> log messages"
def __init__ (self, logger, size=500):
self.size = size
self.logger = logger
self.messages = []
def log (self, message):
self.messages.append (strip_eol (message))
if len (self.messages) > self.size:
del self.messages[0]
self.logger.log (message)
# -*- Mode: Python; tab-width: 4 -*-
# ======================================================================
# Copyright 1997 by Sam Rushing
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Sam
# Rushing not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# ======================================================================
"""socket interface to unix syslog.
On Unix, there are usually two ways of getting to syslog: via a
local unix-domain socket, or via the TCP service.
Usually "/dev/log" is the unix domain socket. This may be different
for other systems.
>>> my_client = syslog_client ('/dev/log')
Otherwise, just use the UDP version, port 514.
>>> my_client = syslog_client (('my_log_host', 514))
On win32, you will have to use the UDP version. Note that
you can use this to log to other hosts (and indeed, multiple
hosts).
This module is not a drop-in replacement for the python
<syslog> extension module - the interface is different.
Usage:
>>> c = syslog_client()
>>> c = syslog_client ('/strange/non_standard_log_location')
>>> c = syslog_client (('other_host.com', 514))
>>> c.log ('testing', facility='local0', priority='debug')
"""
# TODO: support named-pipe syslog.
# [see ftp://sunsite.unc.edu/pub/Linux/system/Daemons/syslog-fifo.tar.z]
# from <linux/sys/syslog.h>:
# ===========================================================================
# priorities/facilities are encoded into a single 32-bit quantity, where the
# bottom 3 bits are the priority (0-7) and the top 28 bits are the facility
# (0-big number). Both the priorities and the facilities map roughly
# one-to-one to strings in the syslogd(8) source code. This mapping is
# included in this file.
#
# priorities (these are ordered)
LOG_EMERG = 0 # system is unusable
LOG_ALERT = 1 # action must be taken immediately
LOG_CRIT = 2 # critical conditions
LOG_ERR = 3 # error conditions
LOG_WARNING = 4 # warning conditions
LOG_NOTICE = 5 # normal but significant condition
LOG_INFO = 6 # informational
LOG_DEBUG = 7 # debug-level messages
# facility codes
LOG_KERN = 0 # kernel messages
LOG_USER = 1 # random user-level messages
LOG_MAIL = 2 # mail system
LOG_DAEMON = 3 # system daemons
LOG_AUTH = 4 # security/authorization messages
LOG_SYSLOG = 5 # messages generated internally by syslogd
LOG_LPR = 6 # line printer subsystem
LOG_NEWS = 7 # network news subsystem
LOG_UUCP = 8 # UUCP subsystem
LOG_CRON = 9 # clock daemon
LOG_AUTHPRIV = 10 # security/authorization messages (private)
# other codes through 15 reserved for system use
LOG_LOCAL0 = 16 # reserved for local use
LOG_LOCAL1 = 17 # reserved for local use
LOG_LOCAL2 = 18 # reserved for local use
LOG_LOCAL3 = 19 # reserved for local use
LOG_LOCAL4 = 20 # reserved for local use
LOG_LOCAL5 = 21 # reserved for local use
LOG_LOCAL6 = 22 # reserved for local use
LOG_LOCAL7 = 23 # reserved for local use
priority_names = {
"alert": LOG_ALERT,
"crit": LOG_CRIT,
"debug": LOG_DEBUG,
"emerg": LOG_EMERG,
"err": LOG_ERR,
"error": LOG_ERR, # DEPRECATED
"info": LOG_INFO,
"notice": LOG_NOTICE,
"panic": LOG_EMERG, # DEPRECATED
"warn": LOG_WARNING, # DEPRECATED
"warning": LOG_WARNING,
}
facility_names = {
"auth": LOG_AUTH,
"authpriv": LOG_AUTHPRIV,
"cron": LOG_CRON,
"daemon": LOG_DAEMON,
"kern": LOG_KERN,
"lpr": LOG_LPR,
"mail": LOG_MAIL,
"news": LOG_NEWS,
"security": LOG_AUTH, # DEPRECATED
"syslog": LOG_SYSLOG,
"user": LOG_USER,
"uucp": LOG_UUCP,
"local0": LOG_LOCAL0,
"local1": LOG_LOCAL1,
"local2": LOG_LOCAL2,
"local3": LOG_LOCAL3,
"local4": LOG_LOCAL4,
"local5": LOG_LOCAL5,
"local6": LOG_LOCAL6,
"local7": LOG_LOCAL7,
}
import socket
class syslog_client:
def __init__ (self, address='/dev/log'):
self.address = address
if type (address) == type(''):
self.socket = socket.socket (socket.AF_UNIX, socket.SOCK_STREAM)
self.socket.connect (address)
self.unix = 1
else:
self.socket = socket.socket (socket.AF_INET, socket.SOCK_DGRAM)
self.unix = 0
# curious: when talking to the unix-domain '/dev/log' socket, a
# zero-terminator seems to be required. this string is placed
# into a class variable so that it can be overridden if
# necessary.
log_format_string = '<%d>%s\000'
def log (self, message, facility=LOG_USER, priority=LOG_INFO):
message = self.log_format_string % (
self.encode_priority (facility, priority),
message
)
if self.unix:
self.socket.send (message)
else:
self.socket.sendto (message, self.address)
def encode_priority (self, facility, priority):
if type(facility) == type(''):
facility = facility_names[facility]
if type(priority) == type(''):
priority = priority_names[priority]
return (facility<<3) | priority
def close (self):
if self.unix:
self.socket.close()
# -*- Mode: Python -*-
# the medusa icon as a python source file.
width = 97
height = 61
data = 'GIF89aa\000=\000\204\000\000\000\000\000\255\255\255\245\245\245ssskkkccc111)))\326\326\326!!!\316\316\316\300\300\300\204\204\000\224\224\224\214\214\214\200\200\200RRR\377\377\377JJJ\367\367\367BBB\347\347\347\000\204\000\020\020\020\265\265\265\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000!\371\004\001\000\000\021\000,\000\000\000\000a\000=\000\000\005\376`$\216di\236h\252\256l\353\276p,\317tm\337x\256\357|m\001@\240E\305\000\364\2164\206R)$\005\201\214\007r\012{X\255\312a\004\260\\>\026\3240\353)\224n\001W+X\334\373\231~\344.\303b\216\024\027x<\273\307\255G,rJiWN\014{S}k"?ti\013EdPQ\207G@_%\000\026yy\\\201\202\227\224<\221Fs$pOjWz\241<r@vO\236\231\233k\247M\2544\203F\177\235\236L#\247\256Z\270,\266BxJ[\276\256A]iE\304\305\262\273E\313\201\275i#\\\303\321\'h\203V\\\177\326\276\216\220P~\335\230_\264\013\342\275\344KF\233\360Q\212\352\246\000\367\274s\361\236\334\347T\341;\341\246\2202\177\3142\211`\242o\325@S\202\264\031\252\207\260\323\256\205\311\036\236\270\002\'\013\302\177\274H\010\324X\002\0176\212\037\376\321\360\032\226\207\244\2674(+^\202\346r\205J\0211\375\241Y#\256f\0127\315>\272\002\325\307g\012(\007\205\312#j\317(\012A\200\224.\241\003\346GS\247\033\245\344\264\366\015L\'PXQl]\266\263\243\232\260?\245\316\371\362\225\035\332\243J\273\332Q\263\357-D\241T\327\270\265\013W&\330\010u\371b\322IW0\214\261]\003\033Va\365Z#\207\213a\030k\2647\262\014p\354\024[n\321N\363\346\317\003\037P\000\235C\302\000\3228(\244\363YaA\005\022\255_\237@\260\000A\212\326\256qbp\321\332\266\011\334=T\023\010"!B\005\003A\010\224\020\220 H\002\337#\020 O\276E\357h\221\327\003\\\000b@v\004\351A.h\365\354\342B\002\011\257\025\\ \220\340\301\353\006\000\024\214\200pA\300\353\012\364\241k/\340\033C\202\003\000\310fZ\011\003V\240R\005\007\354\376\026A\000\000\360\'\202\177\024\004\210\003\000\305\215\360\000\000\015\220\240\332\203\027@\'\202\004\025VpA\000%\210x\321\206\032J\341\316\010\262\211H"l\333\341\200\200>"]P\002\212\011\010`\002\0066FP\200\001\'\024p]\004\027(8B\221\306]\000\201w>\002iB\001\007\340\260"v7J1\343(\257\020\251\243\011\242i\263\017\215\337\035\220\200\221\365m4d\015\016D\251\341iN\354\346Ng\253\200I\240\031\35609\245\2057\311I\302\2007t\231"&`\314\310\244\011e\226(\236\010w\212\300\234\011\012HX(\214\253\311@\001\233^\222pg{% \340\035\224&H\000\246\201\362\215`@\001"L\340\004\030\234\022\250\'\015(V:\302\235\030\240q\337\205\224\212h@\177\006\000\250\210\004\007\310\207\337\005\257-P\346\257\367]p\353\203\271\256:\203\236\211F\340\247\010\3329g\244\010\307*=A\000\203\260y\012\304s#\014\007D\207,N\007\304\265\027\021C\233\207%B\366[m\353\006\006\034j\360\306+\357\274a\204\000\000;'
# -*- Python -*-
# Converted by ./convert_mime_type_table.py from:
# /usr/src2/apache_1.2b6/conf/mime.types
#
content_type_map = \
{
'ai': 'application/postscript',
'aif': 'audio/x-aiff',
'aifc': 'audio/x-aiff',
'aiff': 'audio/x-aiff',
'au': 'audio/basic',
'avi': 'video/x-msvideo',
'bcpio': 'application/x-bcpio',
'bin': 'application/octet-stream',
'cdf': 'application/x-netcdf',
'class': 'application/octet-stream',
'cpio': 'application/x-cpio',
'cpt': 'application/mac-compactpro',
'csh': 'application/x-csh',
'dcr': 'application/x-director',
'dir': 'application/x-director',
'dms': 'application/octet-stream',
'doc': 'application/msword',
'dvi': 'application/x-dvi',
'dxr': 'application/x-director',
'eps': 'application/postscript',
'etx': 'text/x-setext',
'exe': 'application/octet-stream',
'gif': 'image/gif',
'gtar': 'application/x-gtar',
'gz': 'application/x-gzip',
'hdf': 'application/x-hdf',
'hqx': 'application/mac-binhex40',
'htm': 'text/html',
'html': 'text/html',
'ice': 'x-conference/x-cooltalk',
'ief': 'image/ief',
'jpe': 'image/jpeg',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'kar': 'audio/midi',
'latex': 'application/x-latex',
'lha': 'application/octet-stream',
'lzh': 'application/octet-stream',
'man': 'application/x-troff-man',
'me': 'application/x-troff-me',
'mid': 'audio/midi',
'midi': 'audio/midi',
'mif': 'application/x-mif',
'mov': 'video/quicktime',
'movie': 'video/x-sgi-movie',
'mp2': 'audio/mpeg',
'mpe': 'video/mpeg',
'mpeg': 'video/mpeg',
'mpg': 'video/mpeg',
'mpga': 'audio/mpeg',
'mp3': 'audio/mpeg',
'ms': 'application/x-troff-ms',
'nc': 'application/x-netcdf',
'oda': 'application/oda',
'pbm': 'image/x-portable-bitmap',
'pdb': 'chemical/x-pdb',
'pdf': 'application/pdf',
'pgm': 'image/x-portable-graymap',
'png': 'image/png',
'pnm': 'image/x-portable-anymap',
'ppm': 'image/x-portable-pixmap',
'ppt': 'application/powerpoint',
'ps': 'application/postscript',
'qt': 'video/quicktime',
'ra': 'audio/x-realaudio',
'ram': 'audio/x-pn-realaudio',
'ras': 'image/x-cmu-raster',
'rgb': 'image/x-rgb',
'roff': 'application/x-troff',
'rpm': 'audio/x-pn-realaudio-plugin',
'rtf': 'application/rtf',
'rtx': 'text/richtext',
'sgm': 'text/x-sgml',
'sgml': 'text/x-sgml',
'sh': 'application/x-sh',
'shar': 'application/x-shar',
'sit': 'application/x-stuffit',
'skd': 'application/x-koan',
'skm': 'application/x-koan',
'skp': 'application/x-koan',
'skt': 'application/x-koan',
'snd': 'audio/basic',
'src': 'application/x-wais-source',
'sv4cpio': 'application/x-sv4cpio',
'sv4crc': 'application/x-sv4crc',
't': 'application/x-troff',
'tar': 'application/x-tar',
'tcl': 'application/x-tcl',
'tex': 'application/x-tex',
'texi': 'application/x-texinfo',
'texinfo': 'application/x-texinfo',
'tif': 'image/tiff',
'tiff': 'image/tiff',
'tr': 'application/x-troff',
'tsv': 'text/tab-separated-values',
'txt': 'text/plain',
'ustar': 'application/x-ustar',
'vcd': 'application/x-cdlink',
'vrml': 'x-world/x-vrml',
'wav': 'audio/x-wav',
'wrl': 'x-world/x-vrml',
'xbm': 'image/x-xbitmap',
'xpm': 'image/x-xpixmap',
'xwd': 'image/x-xwindowdump',
'xyz': 'chemical/x-pdb',
'zip': 'application/zip',
}
# -*- Mode: Python; tab-width: 4 -*-
RCS_ID = '$Id: producers.py,v 1.1 1999/01/09 03:17:32 amos Exp $'
import string
"""
A collection of producers.
Each producer implements a particular feature: They can be combined
in various ways to get interesting and useful behaviors.
For example, you can feed dynamically-produced output into the compressing
producer, then wrap this with the 'chunked' transfer-encoding producer.
"""
class simple_producer:
"producer for a string"
def __init__ (self, data, buffer_size=1024):
self.data = data
self.buffer_size = buffer_size
def more (self):
if len (self.data) > self.buffer_size:
result = self.data[:self.buffer_size]
self.data = self.data[self.buffer_size:]
return result
else:
result = self.data
self.data = ''
return result
class scanning_producer:
"like simple_producer, but more efficient for large strings"
def __init__ (self, data, buffer_size=1024):
self.data = data
self.buffer_size = buffer_size
self.pos = 0
def more (self):
if self.pos < len(self.data):
lp = self.pos
rp = min (
len(self.data),
self.pos + self.buffer_size
)
result = self.data[lp:rp]
self.pos = self.pos + len(result)
return result
else:
return ''
class lines_producer:
"producer for a list of lines"
def __init__ (self, lines):
self.lines = lines
def ready (self):
return len(self.lines)
def more (self):
if self.lines:
chunk = self.lines[:50]
self.lines = self.lines[50:]
return string.join (chunk, '\r\n') + '\r\n'
else:
return ''
class file_producer:
"producer wrapper for file[-like] objects"
# match http_channel's outgoing buffer size
out_buffer_size = 1<<16
def __init__ (self, file):
self.done = 0
self.file = file
def more (self):
if self.done:
return ''
else:
data = self.file.read (self.out_buffer_size)
if not data:
self.file.close()
del self.file
self.done = 1
return ''
else:
return data
# A simple output producer. This one does not [yet] have
# the safety feature builtin to the monitor channel: runaway
# output will not be caught.
# don't try to print from within any of the methods
# of this object.
class output_producer:
"Acts like an output file; suitable for capturing sys.stdout"
def __init__ (self):
self.data = ''
def write (self, data):
lines = string.splitfields (data, '\n')
data = string.join (lines, '\r\n')
self.data = self.data + data
def writeline (self, line):
self.data = self.data + line + '\r\n'
def writelines (self, lines):
self.data = self.data + string.joinfields (
lines,
'\r\n'
) + '\r\n'
def ready (self):
return (len (self.data) > 0)
def flush (self):
pass
def softspace (self, *args):
pass
def more (self):
if self.data:
result = self.data[:512]
self.data = self.data[512:]
return result
else:
return ''
class composite_producer:
"combine a fifo of producers into one"
def __init__ (self, producers):
self.producers = producers
def more (self):
while len(self.producers):
p = self.producers.first()
d = p.more()
if d:
return d
else:
self.producers.pop()
else:
return ''
class globbing_producer:
"""
'glob' the output from a producer into a particular buffer size.
helps reduce the number of calls to send(). [this appears to
gain about 30% performance on requests to a single channel]
"""
def __init__ (self, producer, buffer_size=1<<16):
self.producer = producer
self.buffer = ''
self.buffer_size = buffer_size
def more (self):
while len(self.buffer) < self.buffer_size:
data = self.producer.more()
if data:
self.buffer = self.buffer + data
else:
break
r = self.buffer
self.buffer = ''
return r
class hooked_producer:
"""
A producer that will call <function> when it empties,.
with an argument of the number of bytes produced. Useful
for logging/instrumentation purposes.
"""
def __init__ (self, producer, function):
self.producer = producer
self.function = function
self.bytes = 0
def more (self):
if self.producer:
result = self.producer.more()
if not result:
self.producer = None
self.function (self.bytes)
else:
self.bytes = self.bytes + len(result)
return result
else:
return ''
# HTTP 1.1 emphasizes that an advertised Content-Length header MUST be
# correct. In the face of Strange Files, it is conceivable that
# reading a 'file' may produce an amount of data not matching that
# reported by os.stat() [text/binary mode issues, perhaps the file is
# being appended to, etc..] This makes the chunked encoding a True
# Blessing, and it really ought to be used even with normal files.
# How beautifully it blends with the concept of the producer.
class chunked_producer:
"""A producer that implements the 'chunked' transfer coding for HTTP/1.1.
Here is a sample usage:
request['Transfer-Encoding'] = 'chunked'
request.push (
producers.chunked_producer (your_producer)
)
request.done()
"""
def __init__ (self, producer, footers=None):
self.producer = producer
self.footers = footers
def more (self):
if self.producer:
data = self.producer.more()
if data:
return '%x\r\n%s\r\n' % (len(data), data)
else:
self.producer = None
if self.footers:
return string.join (
['0'] + self.footers,
'\r\n'
) + '\r\n\r\n'
else:
return '0\r\n\r\n'
else:
return ''
# Unfortunately this isn't very useful right now (Aug 97), because
# apparently the browsers don't do on-the-fly decompression. Which
# is sad, because this could _really_ speed things up, especially for
# low-bandwidth clients (i.e., most everyone).
try:
import zlib
except ImportError:
zlib = None
class compressed_producer:
"""
Compress another producer on-the-fly, using ZLIB
[Unfortunately, none of the current browsers seem to support this]
"""
# Note: It's not very efficient to have the server repeatedly
# compressing your outgoing files: compress them ahead of time, or
# use a compress-once-and-store scheme. However, if you have low
# bandwidth and low traffic, this may make more sense than
# maintaining your source files compressed.
#
# Can also be used for compressing dynamically-produced output.
def __init__ (self, producer, level=5):
self.producer = producer
self.compressor = zlib.compressobj (level)
def more (self):
if self.producer:
cdata = ''
# feed until we get some output
while not cdata:
data = self.producer.more()
if not data:
self.producer = None
return self.compressor.flush()
else:
cdata = self.compressor.compress (data)
return cdata
else:
return ''
class escaping_producer:
"A producer that escapes a sequence of characters"
" Common usage: escaping the CRLF.CRLF sequence in SMTP, NNTP, etc..."
def __init__ (self, producer, esc_from='\r\n.', esc_to='\r\n..'):
self.producer = producer
self.esc_from = esc_from
self.esc_to = esc_to
self.buffer = ''
from asynchat import find_prefix_at_end
self.find_prefix_at_end = find_prefix_at_end
def more (self):
esc_from = self.esc_from
esc_to = self.esc_to
buffer = self.buffer + self.producer.more()
if buffer:
buffer = string.replace (buffer, esc_from, esc_to)
i = self.find_prefix_at_end (buffer, esc_from)
if i:
# we found a prefix
self.buffer = buffer[-i:]
return buffer[:-i]
else:
# no prefix, return it all
self.buffer = ''
return buffer
else:
return buffer
# -*- Mode: Python; tab-width: 4 -*-
#
# Author: Sam Rushing <rushing@nightmare.com>
#
RCS_ID = '$Id: resolver.py,v 1.1 1999/01/09 03:17:32 amos Exp $'
# Fast, low-overhead asynchronous name resolver. uses 'pre-cooked'
# DNS requests, unpacks only as much as it needs of the reply.
# see rfc1035 for details
import string
VERSION = string.split(RCS_ID)[2]
# header
# 1 1 1 1 1 1
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | ID |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# |QR| Opcode |AA|TC|RD|RA| Z | RCODE |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | QDCOUNT |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | ANCOUNT |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | NSCOUNT |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | ARCOUNT |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# question
# 1 1 1 1 1 1
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | |
# / QNAME /
# / /
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | QTYPE |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | QCLASS |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# build a DNS address request, _quickly_
def fast_address_request (host, id=0):
return (
'%c%c' % (chr((id>>8)&0xff),chr(id&0xff))
+ '\001\000\000\001\000\000\000\000\000\000%s\000\000\001\000\001' % (
string.join (
map (
lambda part: '%c%s' % (chr(len(part)),part),
string.split (host, '.')
), ''
)
)
)
def fast_ptr_request (host, id=0):
return (
'%c%c' % (chr((id>>8)&0xff),chr(id&0xff))
+ '\001\000\000\001\000\000\000\000\000\000%s\000\000\014\000\001' % (
string.join (
map (
lambda part: '%c%s' % (chr(len(part)),part),
string.split (host, '.')
), ''
)
)
)
def unpack_name (r,pos):
n = []
while 1:
ll = ord(r[pos])
if (ll&0xc0):
# compression
pos = (ll&0x3f << 8) + (ord(r[pos+1]))
elif ll == 0:
break
else:
pos = pos + 1
n.append (r[pos:pos+ll])
pos = pos + ll
return string.join (n,'.')
def skip_name (r,pos):
s = pos
while 1:
ll = ord(r[pos])
if (ll&0xc0):
# compression
return pos + 2
elif ll == 0:
pos = pos + 1
break
else:
pos = pos + ll + 1
return pos
def unpack_ttl (r,pos):
return reduce (
lambda x,y: (x<<8)|y,
map (ord, r[pos:pos+4])
)
# resource record
# 1 1 1 1 1 1
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | |
# / /
# / NAME /
# | |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | TYPE |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | CLASS |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | TTL |
# | |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | RDLENGTH |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
# / RDATA /
# / /
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
def unpack_address_reply (r):
ancount = (ord(r[6])<<8) + (ord(r[7]))
# skip question, first name starts at 12,
# this is followed by QTYPE and QCLASS
pos = skip_name (r, 12) + 4
if ancount:
# we are looking very specifically for
# an answer with TYPE=A, CLASS=IN (\000\001\000\001)
for an in range(ancount):
pos = skip_name (r, pos)
if r[pos:pos+4] == '\000\001\000\001':
return (
unpack_ttl (r,pos+4),
'%d.%d.%d.%d' % tuple(map(ord,r[pos+10:pos+14]))
)
# skip over TYPE, CLASS, TTL, RDLENGTH, RDATA
pos = pos + 8
rdlength = (ord(r[pos])<<8) + (ord(r[pos+1]))
pos = pos + 2 + rdlength
return 0, None
else:
return 0, None
def unpack_ptr_reply (r):
ancount = (ord(r[6])<<8) + (ord(r[7]))
# skip question, first name starts at 12,
# this is followed by QTYPE and QCLASS
pos = skip_name (r, 12) + 4
if ancount:
# we are looking very specifically for
# an answer with TYPE=PTR, CLASS=IN (\000\014\000\001)
for an in range(ancount):
pos = skip_name (r, pos)
if r[pos:pos+4] == '\000\014\000\001':
return (
unpack_ttl (r,pos+4),
unpack_name (r, pos+10)
)
# skip over TYPE, CLASS, TTL, RDLENGTH, RDATA
pos = pos + 8
rdlength = (ord(r[pos])<<8) + (ord(r[pos+1]))
pos = pos + 2 + rdlength
return 0, None
else:
return 0, None
from counter import counter
import asyncore
import socket
import sys
# This is a UDP (datagram) resolver.
#
# It may be useful to implement a TCP resolver. This would presumably
# give us more reliable behavior when things get too busy. A TCP
# client would have to manage the connection carefully, since the
# server is allowed to close it at will (the RFC recommends closing
# after 2 minutes of idle time).
#
# Note also that the TCP client will have to prepend each request
# with a 2-byte length indicator (see rfc1035).
#
class resolver (asyncore.dispatcher):
id = counter()
def __init__ (self, server='127.0.0.1'):
asyncore.dispatcher.__init__ (self)
self.create_socket (socket.AF_INET, socket.SOCK_DGRAM)
self.server = server
self.request_map = {}
def writable (self):
return 0
def log (self, *args):
pass
def handle_close (self):
print 'closing!'
self.close()
def get_id (self):
return (self.id.as_long() % (1<<16))
def resolve (self, host, callback):
self.socket.sendto (
fast_address_request (host, self.get_id()),
(self.server, 53)
)
self.request_map [self.get_id()] = host, unpack_address_reply, callback
self.id.increment()
def resolve_ptr (self, host, callback):
ip = string.split (host, '.')
ip.reverse()
ip = string.join (ip, '.') + '.in-addr.arpa'
self.socket.sendto (
fast_ptr_request (ip, self.get_id()),
(self.server, 53)
)
self.request_map [self.get_id()] = host, unpack_ptr_reply, callback
self.id.increment()
def handle_read (self):
reply, whence = self.socket.recvfrom (512)
# for security reasons we may want to double-check
# that <whence> is the server we sent the request to.
id = (ord(reply[0])<<8) + ord(reply[1])
if self.request_map.has_key (id):
host, unpack, callback = self.request_map[id]
del self.request_map[id]
ttl, answer = unpack (reply)
try:
callback (host, ttl, answer)
except:
t,v,tb = sys.exc_info()
(file,fun,line), tbinfo = asyncore.compact_traceback (t,v,tb)
print t,v
print tbinfo
class rbl (resolver):
def resolve_maps (self, host, callback):
ip = string.split (host, '.')
ip.reverse()
ip = string.join (ip, '.') + '.rbl.maps.vix.com'
self.socket.sendto (
fast_ptr_request (ip, self.get_id()),
(self.server, 53)
)
self.request_map [self.get_id()] = host, self.check_reply, callback
self.id.increment()
def check_reply (self, r):
# we only need to check RCODE.
rcode = (ord(r[3])&0xf)
print 'MAPS RBL; RCODE =%02x' % rcode
print repr(r)
return 0, rcode # (ttl, answer)
import time
class hooked_callback:
def __init__ (self, hook, callback):
self.hook, self.callback = hook, callback
def __call__ (self, *args):
apply (self.hook, args)
apply (self.callback, args)
class caching_resolver (resolver):
"Cache DNS queries. Will need to honor the TTL value in the replies"
def __init__ (*args):
apply (resolver.__init__, args)
self = args[0]
self.cache = {}
self.forward_requests = counter()
self.reverse_requests = counter()
self.cache_hits = counter()
def resolve (self, host, callback):
self.forward_requests.increment()
if self.cache.has_key (host):
when, ttl, answer = self.cache[host]
# ignore TTL for now
callback (host, ttl, answer)
self.cache_hits.increment()
else:
resolver.resolve (
self,
host,
hooked_callback (
self.callback_hook,
callback
)
)
def resolve_ptr (self, host, callback):
self.reverse_requests.increment()
if self.cache.has_key (host):
when, ttl, answer = self.cache[host]
# ignore TTL for now
callback (host, ttl, answer)
self.cache_hits.increment()
else:
resolver.resolve_ptr (
self,
host,
hooked_callback (
self.callback_hook,
callback
)
)
def callback_hook (self, host, ttl, answer):
self.cache[host] = time.time(), ttl, answer
SERVER_IDENT = 'Caching DNS Resolver (V%s)' % VERSION
def status (self):
import status_handler
import producers
return producers.simple_producer (
'<h2>%s</h2>' % self.SERVER_IDENT
+ '<br>Server: %s' % self.server
+ '<br>Cache Entries: %d' % len(self.cache)
+ '<br>Outstanding Requests: %d' % len(self.request_map)
+ '<br>Forward Requests: %s' % self.forward_requests
+ '<br>Reverse Requests: %s' % self.reverse_requests
+ '<br>Cache Hits: %s' % self.cache_hits
)
#test_reply = """\000\000\205\200\000\001\000\001\000\002\000\002\006squirl\011nightmare\003com\000\000\001\000\001\300\014\000\001\000\001\000\001Q\200\000\004\315\240\260\005\011nightmare\003com\000\000\002\000\001\000\001Q\200\000\002\300\014\3006\000\002\000\001\000\001Q\200\000\015\003ns1\003iag\003net\000\300\014\000\001\000\001\000\001Q\200\000\004\315\240\260\005\300]\000\001\000\001\000\000\350\227\000\004\314\033\322\005"""
# def test_unpacker ():
# print unpack_address_reply (test_reply)
#
# import time
# class timer:
# def __init__ (self):
# self.start = time.time()
# def end (self):
# return time.time() - self.start
#
# # I get ~290 unpacks per second for the typical case, compared to ~48
# # using dnslib directly. also, that latter number does not include
# # picking the actual data out.
#
# def benchmark_unpacker():
#
# r = range(1000)
# t = timer()
# for i in r:
# unpack_address_reply (test_reply)
# print '%.2f unpacks per second' % (1000.0 / t.end())
if __name__ == '__main__':
import sys
if len(sys.argv) == 1:
print 'usage: %s [-r] [-s <server_IP>] host [host ...]' % sys.argv[0]
sys.exit(0)
elif ('-s' in sys.argv):
i = sys.argv.index('-s')
server = sys.argv[i+1]
del sys.argv[i:i+2]
else:
server = '127.0.0.1'
if ('-r' in sys.argv):
reverse = 1
i = sys.argv.index('-r')
del sys.argv[i]
else:
reverse = 0
if ('-m' in sys.argv):
maps = 1
sys.argv.remove ('-m')
else:
maps = 0
if maps:
r = rbl (server)
else:
r = caching_resolver(server)
count = len(sys.argv) - 1
def print_it (host, ttl, answer):
global count
print '%s: %s' % (host, answer)
count = count - 1
if not count:
r.close()
for host in sys.argv[1:]:
if reverse:
r.resolve_ptr (host, print_it)
elif maps:
r.resolve_maps (host, print_it)
else:
r.resolve (host, print_it)
# hooked asyncore.loop()
while asyncore.socket_map:
asyncore.poll (30.0)
print 'requests outstanding: %d' % len(r.request_map)
# -*- Mode: Python; tab-width: 4 -*-
#
# medusa status extension
#
import string
import time
import regex
import asyncore
import http_server
import medusa_gif
import producers
from counter import counter
START_TIME = long(time.time())
# split a uri
# <path>;<params>?<query>#<fragment>
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))
class status_extension:
hit_counter = counter()
hyper_regex = regex.compile ('/status/object/\([0-9]+\)/.*')
def __init__ (self, objects, regexp='/status\(/.*\)?'):
self.objects = objects
self.regexp = regex.compile (regexp)
self.hyper_objects = []
for object in objects:
self.register_hyper_object (object)
def __repr__ (self):
return '<Status Extension (%s hits) at %x>' % (
self.hit_counter,
id(self)
)
def match (self, request):
[path, params, query, fragment] = split_path (request.uri)
return self.regexp.match (path) == len(path)
# Possible Targets:
# /status
# /status/channel_list
# /status/medusa.gif
# can we have 'clickable' objects?
# [yes, we can use id(x) and do a linear search]
# Dynamic producers:
# HTTP/1.0: we must close the channel, because it's dynamic output
# HTTP/1.1: we can use the chunked transfer-encoding, and leave
# it open.
def handle_request (self, request):
[path, params, query, fragment] = split_path (request.uri)
self.hit_counter.increment()
if path == '/status':
up_time = string.join (english_time (long(time.time()) - START_TIME))
request['Content-Type'] = 'text/html'
request.push (
'<html>'
'<title>Medusa Status Reports</title>'
'<body bgcolor="#ffffff">'
'<h1>Medusa Status Reports</h1>'
'<b>Up:</b> %s' % up_time
)
for i in range(len(self.objects)):
request.push (self.objects[i].status())
request.push ('<hr>\r\n')
request.push (
'<p><a href="/status/channel_list">Channel List</a>'
'<hr>'
'<img src="/status/medusa.gif" align=right width=%d height=%d>' % (
medusa_gif.width,
medusa_gif.height
) +
'</body></html>'
)
request.done()
elif path == '/status/channel_list':
request['Content-Type'] = 'text/html'
request.push ('<html><body>')
request.push(channel_list_producer())
request.push (
'<hr>'
'<img src="/status/medusa.gif" align=right width=%d height=%d>' % (
medusa_gif.width,
medusa_gif.height
) +
'</body></html>'
)
request.done()
elif path == '/status/medusa.gif':
request['Content-Type'] = 'image/gif'
request['Content-Length'] = len(medusa_gif.data)
request.push (medusa_gif.data)
request.done()
elif path == '/status/close_zombies':
message = (
'<h2>Closing all zombie http client connections...</h2>'
'<p><a href="/status">Back to the status page</a>'
)
request['Content-Type'] = 'text/html'
request['Content-Length'] = len (message)
request.push (message)
now = int (time.time())
for channel in asyncore.socket_map.keys():
if channel.__class__ == http_server.http_channel:
if channel != request.channel:
if (now - channel.creation_time) > channel.zombie_timeout:
channel.close()
request.done()
# Emergency Debug Mode
# If a server is running away from you, don't KILL it!
# Move all the AF_INET server ports and perform an autopsy...
# [disabled by default to protect the innocent]
#elif path == '/status/emergency_debug':
elif 0:
request.push ('<html>Moving All Servers...</html>')
request.done()
for channel in asyncore.socket_map.keys():
if channel.accepting:
if type(channel.addr) is type(()):
ip, port = channel.addr
channel.socket.close()
channel.del_channel()
channel.addr = (ip, port+10000)
fam, typ = channel.family_and_type
channel.create_socket (fam, typ)
channel.set_reuse_addr()
channel.bind (channel.addr)
channel.listen(5)
elif self.hyper_regex.match (path) != -1:
oid = string.atoi (self.hyper_regex.group (1))
for object in self.hyper_objects:
if id (object) == oid:
if hasattr (object, 'hyper_respond'):
object.hyper_respond (self, path, request)
else:
request.error (404)
return
def status (self):
return producers.simple_producer (
'<li>Status Extension <b>Hits</b> : %s' % self.hit_counter
)
def register_hyper_object (self, object):
if not object in self.hyper_objects:
self.hyper_objects.append (object)
import logger
class logger_for_status (logger.tail_logger):
def status (self):
return 'Last %d log entries for: %s' % (
len (self.messages),
html_repr (self)
)
def hyper_respond (self, sh, path, request):
request['Content-Type'] = 'text/plain'
messages = self.messages[:]
messages.reverse()
request.push (lines_producer (messages))
request.done()
class lines_producer:
def __init__ (self, lines):
self.lines = lines
def ready (self):
return len(self.lines)
def more (self):
if self.lines:
chunk = self.lines[:50]
self.lines = self.lines[50:]
return string.join (chunk, '\r\n') + '\r\n'
else:
return ''
class channel_list_producer (lines_producer):
def __init__ (self):
channel_reprs = map (
lambda x: '&lt;' + repr(x)[1:-1] + '&gt;',
asyncore.socket_map.keys()
)
channel_reprs.sort()
lines_producer.__init__ (
self,
['<h1>Active Channel List</h1>',
'<pre>'
] + channel_reprs + [
'</pre>',
'<p><a href="/status">Status Report</a>'
]
)
# this really needs a full-blown quoter...
def sanitize (s):
if '<' in s:
s = string.join (string.split (s, '<'), '&lt;')
if '>' in s:
s = string.join (string.split (s, '>'), '&gt;')
return s
def html_repr (object):
so = sanitize (repr (object))
if hasattr (object, 'hyper_respond'):
return '<a href="/status/object/%d/">%s</a>' % (id (object), so)
else:
return so
def html_reprs (list, front='', back=''):
reprs = map (
lambda x,f=front,b=back: '%s%s%s' % (f,x,b),
map (lambda x: sanitize (html_repr(x)), list)
)
reprs.sort()
return reprs
# for example, tera, giga, mega, kilo
# p_d (n, (1024, 1024, 1024, 1024))
# smallest divider goes first - for example
# minutes, hours, days
# p_d (n, (60, 60, 24))
def progressive_divide (n, parts):
result = []
for part in parts:
n, rem = divmod (n, part)
result.append (rem)
result.append (n)
return result
# b,k,m,g,t
def split_by_units (n, units, dividers, format_string):
divs = progressive_divide (n, dividers)
result = []
for i in range(len(units)):
if divs[i]:
result.append (format_string % (divs[i], units[i]))
result.reverse()
if not result:
return [format_string % (0, units[0])]
else:
return result
def english_bytes (n):
return split_by_units (
n,
('','K','M','G','T'),
(1024, 1024, 1024, 1024, 1024),
'%d %sB'
)
def english_time (n):
return split_by_units (
n,
('secs', 'mins', 'hours', 'days', 'weeks', 'years'),
( 60, 60, 24, 7, 52),
'%d %s'
)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment