M setup.py +27 -27
@@ 7,42 7,42 @@ from wormhole.version import VERSION
def readme():
- with open('README.rst', encoding = 'utf-8') as readme_file:
- return '\n' + readme_file.read()
+ with open("README.rst", encoding="utf-8") as readme_file:
+ return "\n" + readme_file.read()
setup(
- name='wormhole-proxy',
- version=VERSION.replace('v',''), # normalize version from vd.d to d.d
- author='Chaiwat Suttipongsakul',
- author_email='cwt@bashell.com',
- url='https://bitbucket.org/bashell-com/wormhole',
- license='MIT',
- description='Asynchronous I/O HTTP and HTTPS Proxy on Python >= 3.5.3',
+ name="wormhole-proxy",
+ version=VERSION.replace("v", ""), # normalize version from vd.d to d.d
+ author="Chaiwat Suttipongsakul",
+ author_email="cwt@bashell.com",
+ url="https://bitbucket.org/bashell-com/wormhole",
+ license="MIT",
+ description="Asynchronous I/O HTTP and HTTPS Proxy on Python >= 3.5.3",
long_description=readme(),
- keywords='wormhole asynchronous web proxy',
+ keywords="wormhole asynchronous web proxy",
classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Environment :: Console',
- 'Intended Audience :: Information Technology',
- 'Intended Audience :: System Administrators',
- 'License :: OSI Approved :: MIT License',
- 'Operating System :: POSIX',
- 'Operating System :: Microsoft :: Windows',
- 'Programming Language :: Python :: 3.5.3',
- 'Programming Language :: Python :: 3.6',
- 'Programming Language :: Python :: 3.7',
- 'Programming Language :: Python :: 3.8',
- 'Programming Language :: Python :: 3.9',
- 'Programming Language :: Python :: 3.10',
- 'Programming Language :: Python :: Implementation :: CPython',
- 'Topic :: Internet :: Proxy Servers',
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Console",
+ "Intended Audience :: Information Technology",
+ "Intended Audience :: System Administrators",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: POSIX",
+ "Operating System :: Microsoft :: Windows",
+ "Programming Language :: Python :: 3.5.3",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Topic :: Internet :: Proxy Servers",
],
install_requires=[
'pywin32;platform_system=="Windows"',
'uvloop;platform_system=="Linux"',
],
- packages=['wormhole'],
+ packages=["wormhole"],
include_package_data=True,
- entry_points={'console_scripts': ['wormhole = wormhole.proxy:main']},
+ entry_points={"console_scripts": ["wormhole = wormhole.proxy:main"]},
)
M wormhole/__main__.py +0 -1
@@ 1,4 1,3 @@
from proxy import main
exit(main())
-
M wormhole/authentication.py +26 -17
@@ 2,39 2,48 @@ from base64 import decodebytes
def get_ident(client_reader, client_writer, user=None):
- client = client_writer.get_extra_info('peername')[0]
+ client = client_writer.get_extra_info("peername")[0]
if user:
- client = '%s@%s' % (user, client)
- return {'id': hex(id(client_reader))[-6:], 'client':client}
+ client = "%s@%s" % (user, client)
+ return {"id": hex(id(client_reader))[-6:], "client": client}
auth_list = list()
+
+
def get_auth_list(auth):
global auth_list
if not auth_list:
- auth_list = [line.strip()
- for line in open(auth, 'r')
- if line.strip() and not line.strip().startswith('#')]
+ auth_list = [
+ line.strip()
+ for line in open(auth, "r")
+ if line.strip() and not line.strip().startswith("#")
+ ]
return auth_list
def deny(client_writer):
- [client_writer.write(message)
- for message in (
- b'HTTP/1.1 407 Proxy Authentication Required\r\n',
- b'Proxy-Authenticate: Basic realm="Wormhole Proxy"\r\n',
- b'\r\n'
- )]
+ [
+ client_writer.write(message)
+ for message in (
+ b"HTTP/1.1 407 Proxy Authentication Required\r\n",
+ b'Proxy-Authenticate: Basic realm="Wormhole Proxy"\r\n',
+ b"\r\n",
+ )
+ ]
async def verify(client_reader, client_writer, headers, auth):
- proxy_auth = [header for header in headers
- if header.lower().startswith('proxy-authorization:')]
+ proxy_auth = [
+ header
+ for header in headers
+ if header.lower().startswith("proxy-authorization:")
+ ]
if proxy_auth:
user_password = decodebytes(
- proxy_auth[0].split(' ')[2].encode('ascii')
- ).decode('ascii')
+ proxy_auth[0].split(" ")[2].encode("ascii")
+ ).decode("ascii")
if user_password in get_auth_list(auth):
- user = user_password.split(':')[0]
+ user = user_password.split(":")[0]
return get_ident(client_reader, client_writer, user)
return deny(client_writer)
M wormhole/handler.py +81 -67
@@ 5,8 5,9 @@ from tools import get_content_length
from tools import get_host_and_port
-async def relay_stream(stream_reader, stream_writer,
- ident, return_first_line=False):
+async def relay_stream(
+ stream_reader, stream_writer, ident, return_first_line=False
+):
logger = get_logger()
first_line = None
while True:
@@ 16,22 17,23 @@ async def relay_stream(stream_reader, st
break
stream_writer.write(line)
except Exception as ex:
- error_message = '%s: %s' % (
+ error_message = "%s: %s" % (
ex.__class__.__name__,
- ' '.join([str(arg) for arg in ex.args])
+ " ".join([str(arg) for arg in ex.args]),
)
- logger.debug((
- '[{id}][{client}]: %s' % error_message
- ).format(**ident))
+ logger.debug(
+ ("[{id}][{client}]: %s" % error_message).format(**ident)
+ )
break
else:
if return_first_line and first_line is None:
- first_line = line[:line.find(b'\r\n')]
+ first_line = line[: line.find(b"\r\n")]
return first_line
-async def process_https(client_reader, client_writer, request_method, uri,
- ident):
+async def process_https(
+ client_reader, client_writer, request_method, uri, ident
+):
response_code = 200
error_message = None
host, port = get_host_and_port(uri)
@@ 40,48 42,53 @@ async def process_https(client_reader, c
req_reader, req_writer = await asyncio.open_connection(
host, port, ssl=False
)
- client_writer.write(b'HTTP/1.1 200 Connection established\r\n')
- client_writer.write(b'\r\n')
+ client_writer.write(b"HTTP/1.1 200 Connection established\r\n")
+ client_writer.write(b"\r\n")
# HTTPS need to log here, as the connection may keep alive for long.
- logger.info((
- '[{id}][{client}]: %s %d %s' % (
- request_method, response_code, uri
- )
- ).format(**ident))
+ logger.info(
+ (
+ "[{id}][{client}]: %s %d %s"
+ % (request_method, response_code, uri)
+ ).format(**ident)
+ )
tasks = [
asyncio.ensure_future(
- relay_stream(client_reader, req_writer, ident)),
+ relay_stream(client_reader, req_writer, ident)
+ ),
asyncio.ensure_future(
- relay_stream(req_reader, client_writer, ident)),
+ relay_stream(req_reader, client_writer, ident)
+ ),
]
await asyncio.wait(tasks)
except Exception as ex:
response_code = 502
- error_message = '%s: %s' % (
+ error_message = "%s: %s" % (
ex.__class__.__name__,
- ' '.join([str(arg) for arg in ex.args])
+ " ".join([str(arg) for arg in ex.args]),
)
if error_message:
- logger.error((
- '[{id}][{client}]: %s %d %s (%s)' % (
- request_method, response_code, uri, error_message
- )
- ).format(**ident))
+ logger.error(
+ (
+ "[{id}][{client}]: %s %d %s (%s)"
+ % (request_method, response_code, uri, error_message)
+ ).format(**ident)
+ )
-async def process_http(client_writer, request_method, uri, http_version,
- headers, payload, ident):
+async def process_http(
+ client_writer, request_method, uri, http_version, headers, payload, ident
+):
response_status = None
response_code = None
error_message = None
- hostname = '127.0.0.1' # hostname (with optional port) e.g. example.com:80
+ hostname = "127.0.0.1" # hostname (with optional port) e.g. example.com:80
request_headers = []
request_headers_end_index = 0
has_connection_header = False
for header in headers:
- name_and_value = header.split(': ', 1)
+ name_and_value = header.split(": ", 1)
if len(name_and_value) == 2:
name, value = name_and_value
@@ 93,13 100,13 @@ async def process_http(client_writer, re
hostname = value
elif name.lower() == "connection":
has_connection_header = True
- if value.lower() in ('keep-alive', 'persist'):
+ if value.lower() in ("keep-alive", "persist"):
# current version of this program does not support
# the HTTP keep-alive feature
request_headers.append("Connection: close")
else:
request_headers.append(header)
- elif name.lower() != 'proxy-connection':
+ elif name.lower() != "proxy-connection":
request_headers.append(header)
if len(header) == 0 and request_headers_end_index == 0:
request_headers_end_index = len(request_headers) - 1
@@ 110,8 117,8 @@ async def process_http(client_writer, re
if not has_connection_header:
request_headers.insert(request_headers_end_index, "Connection: close")
- path = uri[len(hostname) + 7:] # 7 is len('http://')
- new_head = ' '.join([request_method, path, http_version])
+ path = uri[len(hostname) + 7 :] # 7 is len('http://')
+ new_head = " ".join([request_method, path, http_version])
host, port = get_host_and_port(hostname, 80)
@@ 119,19 126,21 @@ async def process_http(client_writer, re
req_reader, req_writer = await asyncio.open_connection(
host, port, flags=TCP_NODELAY
)
- req_writer.write(('%s\r\n' % new_head).encode())
+ req_writer.write(("%s\r\n" % new_head).encode())
await req_writer.drain()
- req_writer.write(b'Host: ' + hostname.encode())
- req_writer.write(b'\r\n')
+ req_writer.write(b"Host: " + hostname.encode())
+ req_writer.write(b"\r\n")
- [req_writer.write((header + '\r\n').encode())
- for header in request_headers]
- req_writer.write(b'\r\n')
+ [
+ req_writer.write((header + "\r\n").encode())
+ for header in request_headers
+ ]
+ req_writer.write(b"\r\n")
- if payload != b'':
+ if payload != b"":
req_writer.write(payload)
- req_writer.write(b'\r\n')
+ req_writer.write(b"\r\n")
await req_writer.drain()
response_status = await relay_stream(
@@ 139,34 148,36 @@ async def process_http(client_writer, re
)
except Exception as ex:
response_code = 502
- error_message = '%s: %s' % (
+ error_message = "%s: %s" % (
ex.__class__.__name__,
- ' '.join([str(arg) for arg in ex.args])
+ " ".join([str(arg) for arg in ex.args]),
)
if response_code is None:
- response_code = int(response_status.decode('ascii').split(' ')[1])
+ response_code = int(response_status.decode("ascii").split(" ")[1])
logger = get_logger()
if error_message is None:
- logger.info((
- '[{id}][{client}]: %s %d %s' % (
- request_method, response_code, uri
- )
- ).format(**ident))
+ logger.info(
+ (
+ "[{id}][{client}]: %s %d %s"
+ % (request_method, response_code, uri)
+ ).format(**ident)
+ )
else:
- logger.error((
- '[{id}][{client}]: %s %d %s (%s)' % (
- request_method, response_code, uri, error_message
- )
- ).format(**ident))
+ logger.error(
+ (
+ "[{id}][{client}]: %s %d %s (%s)"
+ % (request_method, response_code, uri, error_message)
+ ).format(**ident)
+ )
async def process_request(client_reader, max_retry, ident):
logger = get_logger()
- request_line = ''
+ request_line = ""
headers = []
- header = ''
- payload = b''
+ header = ""
+ payload = b""
try:
retry = 0
while True:
@@ 180,24 191,27 @@ async def process_request(client_reader,
continue
else:
break
- if line == b'\r\n':
+ if line == b"\r\n":
break
- if line != b'':
+ if line != b"":
header += line.decode()
content_length = get_content_length(header)
while len(payload) < content_length:
payload += await client_reader.read(1024)
except Exception as ex:
- logger.debug((
- '[{id}][{client}]: !!! Task reject (%s: %s)' % (
- ex.__class__.__name__,
- ' '.join([str(arg) for arg in ex.args])
- )
- ).format(**ident))
+ logger.debug(
+ (
+ "[{id}][{client}]: !!! Task reject (%s: %s)"
+ % (
+ ex.__class__.__name__,
+ " ".join([str(arg) for arg in ex.args]),
+ )
+ ).format(**ident)
+ )
if header:
- header_lines = header.split('\r\n')
+ header_lines = header.split("\r\n")
if len(header_lines) > 1:
request_line = header_lines[0]
if len(header_lines) > 2:
M wormhole/license.py +1 -1
@@ 26,5 26,5 @@ SOFTWARE.
"""
-if __name__ == '__main__':
+if __name__ == "__main__":
print(LICENSE)
M wormhole/logger.py +11 -9
@@ 13,25 13,27 @@ class ContextFilter(logging.Filter):
logger = None
+
+
def get_logger(syslog_host=None, syslog_port=514, verbose=0):
- unix_format = '%(asctime)s %(name)s[%(process)d]: %(message)s'
- net_format = '%(asctime)s %(hostname)s %(name)s[%(process)d]: %(message)s'
- date_format = '%b %d %H:%M:%S'
+ unix_format = "%(asctime)s %(name)s[%(process)d]: %(message)s"
+ net_format = "%(asctime)s %(hostname)s %(name)s[%(process)d]: %(message)s"
+ date_format = "%b %d %H:%M:%S"
global logger
if logger is None:
logging.basicConfig(
level=logging.INFO,
- format='%(asctime)s %(name)s[%(process)d]: %(message)s'
+ format="%(asctime)s %(name)s[%(process)d]: %(message)s",
)
- logging.getLogger('asyncio').setLevel(logging.CRITICAL)
- logger = logging.getLogger('wormhole')
+ logging.getLogger("asyncio").setLevel(logging.CRITICAL)
+ logger = logging.getLogger("wormhole")
if verbose >= 1:
logger.setLevel(logging.DEBUG)
if verbose >= 2:
- logging.getLogger('asyncio').setLevel(logging.DEBUG)
- if syslog_host and syslog_host != 'DISABLED':
- if syslog_host.startswith('/') and os.path.exists(syslog_host):
+ logging.getLogger("asyncio").setLevel(logging.DEBUG)
+ if syslog_host and syslog_host != "DISABLED":
+ if syslog_host.startswith("/") and os.path.exists(syslog_host):
syslog = logging.handlers.SysLogHandler(
address=syslog_host,
)
M wormhole/proxy.py +43 -28
@@ 1,15 1,15 @@
#!/usr/bin/env python3
import sys
+
if sys.version_info < (3, 5):
- print('Error: You need python 3.5.0 or above.')
+ print("Error: You need python 3.5.0 or above.")
exit(1)
import os
from pathlib import Path
-sys.path.insert(
- 0, Path(os.path.realpath(__file__)).parent.as_posix()
-)
+
+sys.path.insert(0, Path(os.path.realpath(__file__)).parent.as_posix())
import asyncio
from argparse import ArgumentParser
@@ 24,37 24,53 @@ def main():
port and provides `--help` message.
"""
parser = ArgumentParser(
- description='Wormhole(%s): Asynchronous IO HTTP and HTTPS Proxy' %
- VERSION
+ description="Wormhole(%s): Asynchronous IO HTTP and HTTPS Proxy"
+ % VERSION
)
parser.add_argument(
- '-H', '--host', default='0.0.0.0',
- help='Host to listen [default: %(default)s]'
+ "-H",
+ "--host",
+ default="0.0.0.0",
+ help="Host to listen [default: %(default)s]",
)
parser.add_argument(
- '-p', '--port', type=int, default=8800,
- help='Port to listen [default: %(default)d]'
+ "-p",
+ "--port",
+ type=int,
+ default=8800,
+ help="Port to listen [default: %(default)d]",
)
parser.add_argument(
- '-a', '--authentication', default='',
- help=('File contains username and password list '
- 'for proxy authentication [default: no authentication]')
+ "-a",
+ "--authentication",
+ default="",
+ help=(
+ "File contains username and password list "
+ "for proxy authentication [default: no authentication]"
+ ),
)
parser.add_argument(
- '-S', '--syslog-host', default='DISABLED',
- help='Syslog Host [default: %(default)s]'
+ "-S",
+ "--syslog-host",
+ default="DISABLED",
+ help="Syslog Host [default: %(default)s]",
)
parser.add_argument(
- '-P', '--syslog-port', type=int, default=514,
- help='Syslog Port to listen [default: %(default)d]'
+ "-P",
+ "--syslog-port",
+ type=int,
+ default=514,
+ help="Syslog Port to listen [default: %(default)d]",
)
parser.add_argument(
- '-l', '--license', action='store_true', default=False,
- help='Print LICENSE and exit'
+ "-l",
+ "--license",
+ action="store_true",
+ default=False,
+ help="Print LICENSE and exit",
)
parser.add_argument(
- '-v', '--verbose', action='count', default=0,
- help='Print verbose'
+ "-v", "--verbose", action="count", default=0, help="Print verbose"
)
args = parser.parse_args()
if args.license:
@@ 62,7 78,7 @@ def main():
print(LICENSE)
exit()
if not (1 <= args.port <= 65535):
- parser.error('port must be 1-65535')
+ parser.error("port must be 1-65535")
logger = get_logger(args.syslog_host, args.syslog_port, args.verbose)
try:
@@ 70,15 86,14 @@ def main():
except ImportError:
pass
else:
- logger.debug(
- '[000000][%s]: Using event loop from uvloop.' % args.host
- )
+ logger.debug("[000000][%s]: Using event loop from uvloop." % args.host)
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(
start_wormhole_server(
- args.host, args.port,
+ args.host,
+ args.port,
args.authentication,
)
)
@@ 86,10 101,10 @@ def main():
except OSError:
pass
except KeyboardInterrupt:
- print('bye')
+ print("bye")
finally:
loop.close()
-if __name__ == '__main__':
+if __name__ == "__main__":
exit(main())
M wormhole/server.py +51 -42
@@ 11,15 11,19 @@ from logger import get_logger
MAX_RETRY = 3
-if sys.platform == 'win32':
+if sys.platform == "win32":
import win32file
+
MAX_TASKS = win32file._getmaxstdio()
else:
import resource
+
MAX_TASKS = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
wormhole_semaphore = None
+
+
def get_wormhole_semaphore():
max_wormholes = int(0.9 * MAX_TASKS) # Use only 90% of open files limit.
global wormhole_semaphore
@@ 33,12 37,12 @@ def debug_wormhole_semaphore(client_read
ident = get_ident(client_reader, client_writer)
available = wormhole_semaphore._value
logger = get_logger()
- logger.debug((
- '[{id}][{client}]: Resource available: %.2f%% (%d/%d)' % (
- 100 * float(available) / MAX_TASKS,
- available,
- MAX_TASKS
- )).format(**ident))
+ logger.debug(
+ (
+ "[{id}][{client}]: Resource available: %.2f%% (%d/%d)"
+ % (100 * float(available) / MAX_TASKS, available, MAX_TASKS)
+ ).format(**ident)
+ )
async def process_wormhole(client_reader, client_writer, auth):
@@ 49,59 53,68 @@ async def process_wormhole(client_reader
client_reader, MAX_RETRY, ident
)
if not request_line:
- logger.debug((
- '[{id}][{client}]: !!! Task reject (empty request)'
- ).format(**ident))
+ logger.debug(
+ ("[{id}][{client}]: !!! Task reject (empty request)").format(
+ **ident
+ )
+ )
return
- request_fields = request_line.split(' ')
+ request_fields = request_line.split(" ")
if len(request_fields) == 2:
request_method, uri = request_fields
- http_version = 'HTTP/1.0'
+ http_version = "HTTP/1.0"
elif len(request_fields) == 3:
request_method, uri, http_version = request_fields
else:
- logger.debug((
- '[{id}][{client}]: !!! Task reject (invalid request)'
- ).format(**ident))
+ logger.debug(
+ ("[{id}][{client}]: !!! Task reject (invalid request)").format(
+ **ident
+ )
+ )
return
if auth:
user_ident = await verify(client_reader, client_writer, headers, auth)
if user_ident is None:
- logger.info((
- '[{id}][{client}]: %s 407 %s' % (request_method, uri)
- ).format(**ident))
+ logger.info(
+ ("[{id}][{client}]: %s 407 %s" % (request_method, uri)).format(
+ **ident
+ )
+ )
return
ident = user_ident
- if request_method == 'CONNECT':
+ if request_method == "CONNECT":
async with get_wormhole_semaphore():
debug_wormhole_semaphore(client_reader, client_writer)
return await process_https(
- client_reader, client_writer, request_method, uri,
- ident
+ client_reader, client_writer, request_method, uri, ident
)
else:
async with get_wormhole_semaphore():
debug_wormhole_semaphore(client_reader, client_writer)
return await process_http(
- client_writer, request_method, uri, http_version,
- headers, payload,
- ident
+ client_writer,
+ request_method,
+ uri,
+ http_version,
+ headers,
+ payload,
+ ident,
)
async def limit_wormhole(client_reader, client_writer, auth):
async with get_wormhole_semaphore():
debug_wormhole_semaphore(client_reader, client_writer)
- await process_wormhole(
- client_reader, client_writer, auth
- )
+ await process_wormhole(client_reader, client_writer, auth)
debug_wormhole_semaphore(client_reader, client_writer)
clients = dict()
+
+
def accept_client(client_reader, client_writer, auth):
logger = get_logger()
ident = get_ident(client_reader, client_writer)
@@ 115,34 128,30 @@ def accept_client(client_reader, client_
def client_done(task):
del clients[task]
client_writer.close()
- logger.debug((
- '[{id}][{client}]: Connection closed (%.5f seconds)' % (
- time() - started_time
- )
- ).format(**ident))
+ logger.debug(
+ (
+ "[{id}][{client}]: Connection closed (%.5f seconds)"
+ % (time() - started_time)
+ ).format(**ident)
+ )
- logger.debug((
- '[{id}][{client}]: Connection started'
- ).format(**ident))
+ logger.debug(("[{id}][{client}]: Connection started").format(**ident))
task.add_done_callback(client_done)
async def start_wormhole_server(host, port, auth):
logger = get_logger()
try:
- accept = functools.partial(
- accept_client, auth=auth
- )
+ accept = functools.partial(accept_client, auth=auth)
server = await asyncio.start_server(accept, host, port)
except OSError as ex:
logger.critical(
- '[000000][%s]: !!! Failed to bind server at [%s:%d]: %s' % (
- host, host, port, ex.args[1]
- )
+ "[000000][%s]: !!! Failed to bind server at [%s:%d]: %s"
+ % (host, host, port, ex.args[1])
)
raise
else:
logger.info(
- '[000000][%s]: wormhole bound at %s:%d' % (host, host, port)
+ "[000000][%s]: wormhole bound at %s:%d" % (host, host, port)
)
return server
M wormhole/tools.py +2 -5
@@ 1,13 1,10 @@
import re
-REGEX_HOST = re.compile(
- r'(.+?):([0-9]{1,5})'
-)
+REGEX_HOST = re.compile(r"(.+?):([0-9]{1,5})")
REGEX_CONTENT_LENGTH = re.compile(
- r'\r\nContent-Length: ([0-9]+)\r\n',
- re.IGNORECASE
+ r"\r\nContent-Length: ([0-9]+)\r\n", re.IGNORECASE
)
M wormhole/version.py +1 -2
@@ 1,2 1,1 @@
-VERSION = 'v3.0.0'
-
+VERSION = "v3.0.0"