Created
December 12, 2011 15:16
-
-
Save timesking/1467783 to your computer and use it in GitHub Desktop.
Mini proxy work with Pyhony 2.7
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import asyncore, asynchat | |
import httplib | |
import socket | |
import sys | |
try: | |
from cStringIO import StringIO | |
except ImportError: | |
from StringIO import StringIO | |
from urlparse import urlsplit | |
try: | |
import logging | |
except ImportError: | |
# fake for Python 2.2 | |
class fake: | |
pass | |
logging = fake() | |
logging.DEBUG = 10 | |
logging.anymethod = lambda *args, **kwargs: \ | |
sys.stdout.write(' '.join(map(str,args)) + str(kwargs) + '\n') | |
logging.log = logging.anymethod | |
logging.addLevelName = logging.anymethod | |
logging.basicConfig = logging.anymethod | |
reply_template = """HTTP/1.0 200 OK | |
Content-Type: text/html | |
Connection: close | |
<html> | |
<h3>%s</h3> | |
<hr> | |
<address>miniproxy at %s</address> | |
</html> | |
""" | |
READING_CODE = 0 | |
READING_HEADERS = 1 | |
READING_BODY = 2 | |
REPLYING = 3 | |
TRACE = logging.DEBUG - 1 | |
_no_error = 'UGLY_MARKER' | |
class http_peer(asynchat.async_chat): | |
def __init__(self, parent, conn): | |
logging.log(TRACE, "new %s spawned as %d" % | |
(self.__class__.__name__, id(self))) | |
asynchat.async_chat.__init__(self, sock=conn) | |
self.parent = parent | |
self.ibuffer = [] | |
self.state = READING_CODE | |
self.set_terminator("\r\n") | |
self.log = parent.log | |
def collect_incoming_data(self, data): | |
if self.state == READING_CODE \ | |
or self.state == READING_HEADERS \ | |
or self.state == READING_BODY: | |
self.ibuffer.append(data) | |
# otherwise discard | |
# TODO: handle next requests over keepalive connection | |
def get_ibuffer(self): | |
try: | |
return "".join(self.ibuffer).strip() | |
finally: | |
self.ibuffer = [] | |
class http_server_channel(http_peer): | |
def __init__(self, parent, conn): | |
http_peer.__init__(self, parent, conn) | |
self.raw_request = '' | |
self.raw_headers = '' | |
self.raw_body = '' | |
self.op = '' | |
self.URI = '' | |
self.proto = '' | |
self.headers = None | |
self.request = '' | |
def found_terminator(self): | |
if self.state == READING_CODE: | |
self.raw_request = self.get_ibuffer() | |
try: | |
(self.op, self.URI, self.proto) = self.raw_request.split() | |
except ValueError: | |
self.op = 'BAD_REQUEST' | |
self.reply("malformed request: <%s>" % self.raw_request) | |
self.state = REPLYING | |
self.close_when_done() | |
return | |
self.state = READING_HEADERS | |
self.set_terminator("\r\n\r\n") | |
elif self.state == READING_HEADERS: | |
self.raw_headers = self.get_ibuffer() | |
self.parse_headers(self.raw_headers) | |
if self.op.upper() == "POST": | |
self.state = READING_BODY | |
clen = self.headers.getheader("content-length") | |
self.set_terminator(int(clen)) | |
elif self.op.upper() == 'GET': | |
self.state = REPLYING | |
self.set_terminator(None) | |
self.handle_request() | |
else: | |
# TODO: return 'unsupported method' message | |
self.state = REPLYING | |
self.close_when_done() | |
elif self.state == READING_BODY: | |
self.state = REPLYING | |
self.raw_body = self.get_ibuffer() | |
self.set_terminator(None) | |
self.handle_request() | |
def parse_headers(self, headers): | |
self.headers = httplib.HTTPMessage(StringIO(headers + '\r\n')) | |
# TODO: handle keep-alive connections | |
self.headers['Connection'] = 'close' | |
# detect looping requests to proxy itself | |
proxy_id = str(id(self.parent)) | |
mark = self.headers.get('X-Forwarded-By') | |
if not mark: | |
self.headers['X-Forwarded-By'] = proxy_id | |
elif mark.count(proxy_id): | |
self.headers['X-Loop-Detected'] = proxy_id | |
else: | |
self.headers['X-Forwarded-By'] = ','.join((mark, proxy_id)) | |
def reply(self, message): | |
self.push(reply_template % (message, self.parent.gethostname())) | |
self.close_when_done() | |
def handle_request(self): | |
logging.log(TRACE, "got request: %s" % self.raw_request) | |
self.request = '\r\n'.join((self.raw_request, str(self.headers), | |
'', self.raw_body)) | |
(nil, server, nil, nil, nil) = urlsplit(self.URI, 'http') | |
if server == 'woodoo.curse': | |
self.reply('Killed by wodoo, exiting') | |
self.parent.stop_detected() | |
return | |
if not server or self.headers.get('x-loop-detected'): | |
self.reply('This proxy cannot serve requests to itself') | |
return | |
if server.count(':'): | |
# TODO: handle bad data | |
(server, target_port) = server.split(':') | |
target_port = int(target_port) | |
else: | |
target_port = 80 | |
# todo: handle https protocol | |
http_client_channel(self, server, target_port, self.request) | |
def request_completed(self, error=_no_error): | |
# TODO: filter HTML only | |
self.log('got request \n' + self.request) | |
if error != _no_error: | |
self.reply('Connection failed: "%s"' % error) | |
self.close_when_done() | |
class http_client_channel(http_peer): | |
def __init__(self, parent, server, port, request): | |
http_peer.__init__(self, parent, None) | |
# self.state = READING_HEADERS | |
self.request = request | |
self.clen = 0 | |
self.chunked = 0 | |
self.create_socket(socket.AF_INET, socket.SOCK_STREAM) | |
try: | |
self.connect((server, port)) | |
except: | |
logging.log(TRACE, "%d: failed to connect with %s" % | |
(id(self), server)) | |
socket_errors = getattr(socket, 'errorTab', {}) | |
socket_errors.update({ | |
10014: 'Permission denied', | |
10024: 'Too many open files', | |
10039: 'Destination address required', | |
10040: 'Message too long', | |
10043: 'Protocol not supported', | |
10048: 'Address already in use', | |
10050: 'Network is down', | |
10051: 'Network is unreachable', | |
10052: 'Network dropped connection on reset', | |
10053: 'Software caused connection abort', | |
10054: 'Connection reset by peer', | |
10055: 'No buffer space available', | |
10057: 'Socket is not connected', | |
10058: 'Cannot send after socket shutdown', | |
10060: 'Connection timed out', | |
10061: 'Connection refused', | |
10067: 'Too many processes', | |
10091: 'Network subsystem is unavailable', | |
10092: 'Winsock.dll version out of range', | |
10093: 'Successful WSAStartup not yet performed', | |
10101: 'Graceful shutdown in progress', | |
11001: '(DNS) Host not found', | |
11003: '(DNS) This is a nonrecoverable error', | |
11004: '(DNS) Valid name, no data record of requested type', | |
}) | |
def handle_expt(self): | |
# there is no OOB data in HTTP server response | |
# but there is some errors in asyncore, | |
# leading here after unsuccessful connect | |
e = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) | |
msg = self.socket_errors.get(e, 'Socket error %d' % e) | |
self.handle_close(error=msg) | |
def handle_connect(self): | |
http_peer.handle_connect(self) | |
self.push(self.request) | |
self.set_terminator(None) | |
logging.log(TRACE, "%d: connected with %s" % | |
(id(self), str(self.socket.getpeername()))) | |
def handle_close(self, error=_no_error): | |
self.parent.request_completed(error) | |
logging.log(TRACE, "%d: closed" % id(self)) | |
http_peer.handle_close(self) | |
def collect_incoming_data(self, data): | |
# http_peer.collect_incoming_data(self, data) | |
self.parent.push(data) | |
logging.log(TRACE, "%d: returned %d bytes of data" % | |
(id(self), len(data))) | |
def found_terminator(self): | |
return | |
# if self.state == READING_HEADERS: | |
# field = self.get_ibuffer().lower() | |
# if field == '\r\n': | |
# self.state = READING_BODY | |
# self.set_terminator(None) | |
# if self.clen: | |
# self.set_terminator(self.clen) | |
# elif field.startswith('content-length: '): | |
# self.clen = int(field.split('content-length: ')[1]) | |
# self.set_terminator(self.clen) | |
# elif field.startswith('transfer-encoding: '): | |
# trenc = field.split('transfer-encoding: ')[1] | |
# if trenc == 'chunked': | |
# self.chunked = 1 | |
# elif self.state == READING_BODY: | |
# pass | |
# if self.clen: | |
# self.close() | |
# elif self.chunked: | |
# TODO: track chunked body and close connection, when done | |
# pass | |
class http_proxy(asyncore.dispatcher): | |
def __init__(self, port): | |
self.port = port | |
asyncore.dispatcher.__init__ (self) | |
self.create_socket(socket.AF_INET, socket.SOCK_STREAM) | |
self.set_reuse_addr() | |
self.bind(('', port)) | |
logging.log(TRACE, "binded to port %d" % port) | |
self.listen(1024) | |
def handle_accept(self): | |
(conn, addr) = self.accept() | |
http_server_channel(self, conn) | |
def stop_detected(self): | |
self.close() | |
def gethostname(self): | |
return socket.gethostname() + ':' + str(self.addr[1]) | |
if __name__ == '__main__': | |
# TODO: handle command line | |
logging.addLevelName(TRACE, 'TRACE') | |
logging.basicConfig(level=TRACE) | |
main = http_proxy(8888) | |
asyncore.loop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment