Skip to content

Instantly share code, notes, and snippets.

@timesking
Created December 12, 2011 15:16
Show Gist options
  • Save timesking/1467783 to your computer and use it in GitHub Desktop.
Save timesking/1467783 to your computer and use it in GitHub Desktop.
Mini proxy work with Pyhony 2.7
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