python实现的http服务器

#!/usr/bin/env python
# author:  Hua Liang [ Stupid ET ]
# email:   et@everet.org
# website: http://EverET.org
#

# Rule Of Optimization: Prototype before polishing. Get it
#                       working before you optimize it.

import socket, os, threading, sys, signal, stat
import time, struct, re, traceback
import pprint
from collections import defaultdict

host = 'localhost'
port = 8080
timeout = 15
DOCUMENT_ROOT = os.getcwd() + '/'

HTTP_PROTOCOL = 'HTTP/1.1'

cgiexts = ['cgi', 'php', 'sh', 'py']

mimes = {"application/ogg":      " ogg",
         "application/pdf":      " pdf",
         "application/xml":      " xsl xml",
         "application/xml-dtd":  " dtd",
         "application/xslt+xml": " xslt",
         "application/zip":      " zip",
         "audio/mpeg":           " mp2 mp3 mpga",
         "image/gif":            " gif",
         "image/jpeg":           " jpeg jpe jpg",
         "image/png":            " png",
         "text/css":             " css",
         "text/html":            " html htm",
         "text/javascript":      " js",
         "text/plain":           " txt asc",
         "video/mpeg":           " mpeg mpe mpg",
         "video/quicktime":      " qt mov",
         "video/x-msvideo":      " avi",}

# refine mimes for better use
mm = {}
for t in mimes.keys():
    for ext in mimes[t].split():
        mm[ext] = t
mimes = mm 

default_files = set([
    'index.html',
    'index.php',
    ])

def handle_php(conn):
    handle_cgi(conn)

handlers = {}

class Request(object):
    def __init__(self, header):
        self.request = ''
        self.uri = ''
        self.orig_uri = ''
        self.http_method = ''
        self.http_version = ''
        self.request_line = ''
        self.headers = defaultdict(list)
        self.content_length = -1
        self.body = ''
        self.query_string = ''

        self._parse(header)

    def _parse(self, header):
        lines = header.splitlines()
        self.request_line = lines[0]
        method, uri, protocol = self.request_line.split()

        self.orig_uri = self.uri = uri
        qpos = uri.find('?')
        if qpos != -1:
            self.query_string = uri[qpos + 1:]
            self.uri = uri[:qpos]
        
        self.http_method = method
        self.http_version = protocol 

        for i in range(1, len(lines)):
            key, value = lines[i].split(': ')
            self.headers[key].append(value)

        self.content_length = self.headers.get('Content-Length', [-1])[0]

class Response(object):
    RESPONSE_FROM_FILE = 0
    RESPONSE_FROM_MEM = 1

    def __init__(self):
        self.content_length = -1
        self.keepalive = False
        self.headers = defaultdict(list)
        self.response_type = Response.RESPONSE_FROM_MEM
        self.response = ''
        self.response_fd = -1 
        
class Connection(object):
    def __init__(self, sockfd, remote_ip):
        self.sockfd = sockfd
        self.remote_ip = remote_ip
        self.keepalive = False

        self.reset()

    def reset(self):
        self.state = None
        self.keepalive = False
        self.http_status = -1
        self.request = None
        self.response = None
        self.environment = {}

class ThreadRun(threading.Thread):
    def __init__(self, conn):
        threading.Thread.__init__(self)
        self.conn = conn
    def run(self):
        handle_connection(self.conn)
        self.conn.sockfd.close()
        print '[', self.getName(), ']', 'ended'

class MultiThreadServer(object): 
    def __init__(self, host, port):
        self.listenfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.listenfd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.listenfd.bind((host, port))
        self.listenfd.listen(5) 

    def serve_forver(self):
        while True:
            clientfd, clientaddr = self.listenfd.accept() 

            # timeout for 5 seconds
            clientfd.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
                                struct.pack('ll', timeout, 0))

            # select, fork or multithread
            conn = Connection(clientfd, clientaddr[0]) 

            th = ThreadRun(conn)
            th.start()

def get_header(buf):
    'return header and end pos of header'
    r = re.search(r'\r*\n\r*\n', buf)
    header = buf[:r.start()]
    return header, r.end()

####################

def get_mime(ext):
    'Get mime type by extension, ignore case'
    return mimes.get(ext.lower(), 'application/octet-stream')

def cgi_response_parse(conn, cgi_response):
    print '=' * 50
    # print cgi_response

    if cgi_response.startswith('HTTP/1.'):
        if (cgi_response[7] == '1' or cgi_response[7] == '0') and cgi_response[8] == ' ':
            status = cgi_response[8:10]
            status = int(status)
            if 0 <= status < 1000:
                # good
                conn.http_status = status
    else:
        separtor = re.search(r'\r?\n\r?\n', cgi_response)
        header = cgi_response[:separtor.start()]
        headers = defaultdict(list)
        for line in header.splitlines():
            key, value = line.split(': ')
            headers[key].append(value)

        print 'cgi_response_parse', '|' * 100
        pprint.pprint(headers)

        value = headers.get('Status')
        if value:
            value = value[0]
            conn.http_status = int(value[:3])
            conn.http_msg = value[4:]
            del headers['Status']

        value = headers.get('Connection', [''])[0]
        conn.keepalive = True if value.lower() == 'keep-alive' else False 

        response = Response()
        response.response_type = Response.RESPONSE_FROM_MEM
        response.keepalive = conn.keepalive
        response.headers = headers
        response.response = cgi_response[separtor.end():]

        conn.response = response


def handle_cgi(conn):
    print 'handle_cgi'
    from_cgi_read_end, from_cgi_write_end = os.pipe()
    try:
        to_cgi_read_end, to_cgi_write_end = os.pipe()
    except:
        os.close(from_cgi_read_end)
        os.close(from_cgi_write_end)

    try:
        pid = os.fork()
    except:
        return -1
    if pid == 0: # child
        filename = os.path.normpath(DOCUMENT_ROOT + conn.request.uri)
        print 'CC' * 50
        print filename

        cgienv = {'SERVER_SOFTWARE': 'ethttpd-py',
                  'SERVER_NAME': 'localhost',
                  'SERVER_PORT': str(port),
                  'GATEWAY_INTERFACE': 'CGI/1.1',
                  'SERVER_PROTOCOL': 'HTTP/1.1',}

        cgienv['REQUEST_METHOD'] = conn.request.http_method
        # SCRIPT_FILENAME is VERY important.
        cgienv['SCRIPT_FILENAME'] = filename
        cgienv['SCRIPT_NAME'] = os.path.basename(filename)
        cgienv['REMOTE_ADDR'] = conn.remote_ip
        cgienv['DOCUMENT_ROOT'] = DOCUMENT_ROOT
        cgienv['REDIRECT_STATUS'] = '200'
        cgienv['REQUEST_URI'] = conn.request.orig_uri
        cgienv['HTTP_HOST'] = conn.request.headers['Host'][0]
        cgienv['HTTP_CONNECTION'] = 'Keep-Alive' if conn.keepalive else 'close'

        attrs = conn.request.headers
        if conn.request.query_string:
            cgienv['QUERY_STRING'] = conn.request.query_string
        if attrs.get('Content-Length'):
            cgienv['CONTENT_LENGTH'] = attrs['Content-Length'][0]
        if attrs.get('Content-Type'):
            cgienv['CONTENT_TYPE'] = attrs['Content-Type'][0]
        if attrs.get('Referer'):
            cgienv['HTTP_REFERER'] = attrs['Referer'][0]
        if attrs.get('Cookie'):
            cgienv['HTTP_COOKIE'] = attrs['Cookie'][0]

        # pprint.pprint(vars(conn))
        pprint.pprint(cgienv)

        # move stdout to from_cgi_write_end
        os.close(sys.stdout.fileno())
        os.dup2(from_cgi_write_end, sys.stdout.fileno())
        os.close(from_cgi_write_end)
        # not needed
        os.close(from_cgi_read_end)

        # move stdin to to_cgi_read_end
        os.close(sys.stdin.fileno())
        os.dup2(to_cgi_read_end, sys.stdin.fileno())
        os.close(to_cgi_read_end)
        # not needed
        os.close(to_cgi_write_end)

        if filename.endswith('.php'):
            os.execve('/usr/bin/php5-cgi', ['php5-cgi', filename], cgienv)
        else:
            os.execve(filename, (filename, ), cgienv)

        os.abort()
    else: # parent
        try:
            os.close(to_cgi_read_end)
            os.close(from_cgi_write_end)
            if conn.request.http_method == 'POST':
               # print 'post ' * 50 
               # print conn.body
               # print '#' * 30
               os.write(to_cgi_write_end, conn.request.body)
            response = ''
            isfirst = True
            while True:
                data = os.read(from_cgi_read_end, 4096)
                # print '+-' * 40
                # print data
                # print '+=' * 40
                if not data:
                    print 'return not data'
                    break
                response += data
            os.close(to_cgi_write_end)
            os.close(from_cgi_read_end)
            print '_+' * 20
        except:
            traceback.print_exc()

        # print '[response]' * 5
        # print response
        cgi_response_parse(conn, response)

    return 0 

def make_direct_reply(conn, http_status, msg, html):
    response = Response()
    response.response_type = Response.RESPONSE_FROM_MEM
    response.response = html
    response.headers['Content-Type'].append('text/html')
    response.headers['Content-Length'].append(str(len(html)))
    response.content_length = len(html)

    conn.http_status = http_status
    conn.response = response 

def handle_request(conn):
    filename = os.path.normpath(DOCUMENT_ROOT + conn.request.uri)

    print 'AB' * 50
    print filename

    # check whether the file exists
    if not os.path.isfile(filename):
        # try to add normal file to the end of uri
        for name in default_files:
            test_name = os.path.join(filename, name)
            if os.path.isfile(test_name):
                conn.request.uri = os.path.join(conn.request.uri, name)
                filename = test_name
                break
        else:
            make_direct_reply(conn, 404, 'Not Found',
                              '404 Not Found You Wanted')
            return

    ext = os.path.splitext(filename)[1]
    ext = ext[1:] if ext.startswith('.') else ext

    # check if there's special handler for this file
    # ...... 
    if ext in handlers.keys():
        handlers[ext](conn)
        return

    # ok, it's a normal static file
    # privilege
    try:
        f = open(filename, 'rb')
    except IOError, e:
        make_direct_reply(conn, 403, 'Forbidden',
                          'Permision Denied')
        return

    file_status = os.stat(filename) 
    file_size = file_status[stat.ST_SIZE]
    modified_date = file_status[stat.ST_MTIME]

    # static file
    conn.http_status = 200 

    response = Response()
    response.response_type = Response.RESPONSE_FROM_FILE
    response.response_fd = f
    response.content_length = file_size
    response.headers['Content-Type'].append(get_mime(ext))
    response.headers['Content-Length'].append(str(file_size))

    conn.response = response

def read_request(conn):
    data = conn.sockfd.recv(4096)
    header, header_end_pos = get_header(data)

    request = Request(header)

    if request.http_method == 'POST':
        weWant = int(request.content_length)
        weHad = len(data) - header_end_pos

        print 'weWant', weWant
        print 'weHad', weHad

        to_read = weWant - weHad

        body = data[header_end_pos:] 
        if to_read > 0:
            print 'fuck' * 411
            tail = conn.sockfd.recv(to_read)
            body += tail

        request.body = body 

    conn.request = request

    conn.keepalive = True if \
        request.headers.get('Connection', [''])[0].lower() == 'keep-alive' else False

def response_request(conn):
    r = conn.response
    print '[response_request]'
    # pprint.pprint(vars(r))
    # pprint.pprint(vars(conn))

    status_line = '%s %d %s\r\n' % (
        HTTP_PROTOCOL, conn.http_status, 'Fuck')
    headers = r.headers
    # headers = '\r\n'.join((': '.join((key, headers[key])) for key in headers))

    header_text = ''
    for key in headers:
        for v in headers[key]:
            header_text += ''.join((key, ': ', v, '\r\n'))
    header_text += '\r\n'

    print 'X' * 100
    print header_text

    conn.sockfd.send(status_line)
    conn.sockfd.send(header_text)
    # conn.sockfd.send('\r\n\r\n')

    if r.response_type == Response.RESPONSE_FROM_MEM:
        conn.sockfd.send(r.response)
    elif r.response_type == Response.RESPONSE_FROM_FILE:
        while True:
            data = r.response_fd.read(8192)
            if len(data) == 0: break
            conn.sockfd.send(data)
        r.response_fd.close()
        r.response_fd = -1

def handle_connection(conn):
    try:
        while True:
            conn.reset()

            read_request(conn)

            handle_request(conn)

            if conn.keepalive:
                conn.response.headers['Connection'].append('Keep-Alive')
                conn.response.headers['Keep-Alive'].append('timeout=%d' % (timeout, ))

            response_request(conn)

            if not conn.keepalive:
                break
    except socket.error:
        print '{socket.error connection die}'
    except Exception, e:
        traceback.print_exc()


if __name__ == '__main__':
    handlers = {
        'php': handle_php,
        'py': handle_cgi,
        }

    server = MultiThreadServer(host, port)
    server.serve_forver()
原文地址:https://www.cnblogs.com/hzhida/p/2635484.html