Update
No files matched your search
@@ -0,0 +1,373 @@
|
||||
from httpx import AsyncClient, TimeoutException
|
||||
from colorama import init, Fore
|
||||
from packaging import version
|
||||
from itertools import cycle
|
||||
import configparser
|
||||
import threading
|
||||
import logoprint
|
||||
import argparse
|
||||
import logging
|
||||
import asyncio
|
||||
import socket
|
||||
import base64
|
||||
import getip
|
||||
import httpx
|
||||
import time
|
||||
import re
|
||||
|
||||
init(autoreset=True)
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
def load_config(config_file='config.ini'):
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_file, encoding='utf-8')
|
||||
return config['SETTINGS']
|
||||
|
||||
def load_proxies(file_path='ip.txt'):
|
||||
with open(file_path, 'r') as file:
|
||||
return [line.strip() for line in file if '://' in line]
|
||||
|
||||
class AsyncProxyServer:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.username = config.get('username', '').strip()
|
||||
self.password = config.get('password', '').strip()
|
||||
self.auth_required = bool(self.username and self.password)
|
||||
self.mode = config.get('mode', 'cycle')
|
||||
self.interval = config.getint('interval', 300)
|
||||
self.use_getip = config.getboolean('use_getip', False)
|
||||
self.proxy_file = config.get('proxy_file', 'ip.txt')
|
||||
self.proxies = self.load_proxies()
|
||||
self.initial_proxy = self.proxies[0] if self.proxies else "没有可用的代理"
|
||||
self.proxy_cycle = cycle(self.proxies)
|
||||
self.current_proxy = self.initial_proxy
|
||||
self.last_switch_time = time.time()
|
||||
self.rate_limiter = asyncio.Queue(maxsize=3000)
|
||||
|
||||
def load_proxies(self):
|
||||
if self.use_getip:
|
||||
return [getip.newip()]
|
||||
else:
|
||||
return load_proxies(self.proxy_file)
|
||||
|
||||
async def get_next_proxy(self):
|
||||
current_time = time.time()
|
||||
if current_time - self.last_switch_time >= self.interval:
|
||||
if self.use_getip:
|
||||
self.current_proxy = getip.newip()
|
||||
else:
|
||||
self.current_proxy = next(self.proxy_cycle)
|
||||
self.last_switch_time = current_time
|
||||
logging.info(f"切换到新的代理: {self.current_proxy}")
|
||||
return self.current_proxy
|
||||
|
||||
def time_until_next_switch(self):
|
||||
return max(0, self.interval - (time.time() - self.last_switch_time))
|
||||
|
||||
async def acquire(self):
|
||||
await self.rate_limiter.put(None)
|
||||
await asyncio.sleep(0.001)
|
||||
self.rate_limiter.get_nowait()
|
||||
|
||||
async def handle_client(self, reader, writer):
|
||||
try:
|
||||
await asyncio.shield(self._handle_client_impl(reader, writer))
|
||||
except asyncio.CancelledError:
|
||||
logging.info("客户端处理被取消")
|
||||
except Exception as e:
|
||||
logging.error(f"客户端处理出错: {e}")
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
async def _handle_client_impl(self, reader, writer):
|
||||
try:
|
||||
await self.acquire()
|
||||
request_line = await reader.readline()
|
||||
if not request_line:
|
||||
return
|
||||
|
||||
try:
|
||||
method, path, _ = request_line.decode('utf-8', errors='ignore').split()
|
||||
except ValueError:
|
||||
logging.error(f"无效的请求行: {request_line}")
|
||||
return
|
||||
|
||||
headers = {}
|
||||
while True:
|
||||
line = await reader.readline()
|
||||
if line == b'\r\n':
|
||||
break
|
||||
if line == b'':
|
||||
return
|
||||
try:
|
||||
name, value = line.decode('utf-8', errors='ignore').strip().split(': ', 1)
|
||||
headers[name.lower()] = value
|
||||
except ValueError:
|
||||
logging.error(f"无效的请求行: {line}")
|
||||
continue
|
||||
|
||||
if self.auth_required and not self._authenticate(headers):
|
||||
writer.write(b'HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="Proxy"\r\n\r\n')
|
||||
await writer.drain()
|
||||
return
|
||||
|
||||
if method == 'CONNECT':
|
||||
await self._handle_connect(path, reader, writer)
|
||||
else:
|
||||
await self._handle_request(method, path, headers, reader, writer)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logging.error(f"处理客户端请求时出错: {e}")
|
||||
|
||||
def _authenticate(self, headers):
|
||||
if not self.auth_required:
|
||||
return True
|
||||
|
||||
auth = headers.get('proxy-authorization')
|
||||
if not auth:
|
||||
return False
|
||||
try:
|
||||
scheme, credentials = auth.split()
|
||||
if scheme.lower() != 'basic':
|
||||
return False
|
||||
username, password = base64.b64decode(credentials).decode().split(':')
|
||||
return username == self.username and password == self.password
|
||||
except:
|
||||
return False
|
||||
|
||||
async def _handle_connect(self, path, reader, writer):
|
||||
try:
|
||||
host, port = path.split(':')
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
logging.error(f"Invalid CONNECT path: {path}")
|
||||
writer.write(b'HTTP/1.1 400 Bad Request\r\n\r\n')
|
||||
await writer.drain()
|
||||
return
|
||||
|
||||
proxy = await self.get_next_proxy()
|
||||
proxy_type, proxy_addr = proxy.split('://')
|
||||
proxy_auth, proxy_host_port = self._split_proxy_auth(proxy_addr)
|
||||
proxy_host, proxy_port = proxy_host_port.split(':')
|
||||
proxy_port = int(proxy_port)
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
remote_reader, remote_writer = await asyncio.open_connection(proxy_host, proxy_port)
|
||||
|
||||
if proxy_type == 'http':
|
||||
connect_headers = [f'CONNECT {host}:{port} HTTP/1.1', f'Host: {host}:{port}']
|
||||
if proxy_auth:
|
||||
auth_header = f'Proxy-Authorization: Basic {base64.b64encode(proxy_auth.encode()).decode()}'
|
||||
connect_headers.append(auth_header)
|
||||
connect_request = '\r\n'.join(connect_headers) + '\r\n\r\n'
|
||||
remote_writer.write(connect_request.encode())
|
||||
await remote_writer.drain()
|
||||
response = await remote_reader.readline()
|
||||
if not response.startswith(b'HTTP/1.1 200'):
|
||||
raise Exception("Bad Gateway")
|
||||
while (await remote_reader.readline()) != b'\r\n':
|
||||
pass
|
||||
elif proxy_type in ['socks4', 'socks5']:
|
||||
if proxy_type == 'socks4':
|
||||
remote_writer.write(b'\x04\x01' + port.to_bytes(2, 'big') + socket.inet_aton(host) + b'\x00')
|
||||
else:
|
||||
remote_writer.write(b'\x05\x01\x00')
|
||||
await remote_writer.drain()
|
||||
if (await remote_reader.read(2))[1] == 0:
|
||||
remote_writer.write(b'\x05\x01\x00\x03' + len(host).to_bytes(1, 'big') + host.encode() + port.to_bytes(2, 'big'))
|
||||
await remote_writer.drain()
|
||||
if (await remote_reader.read(10))[1] != 0:
|
||||
raise Exception("Bad Gateway")
|
||||
else:
|
||||
raise Exception("Unsupported proxy type")
|
||||
|
||||
writer.write(b'HTTP/1.1 200 Connection Established\r\n\r\n')
|
||||
await writer.drain()
|
||||
|
||||
await asyncio.gather(
|
||||
self._pipe(reader, remote_writer),
|
||||
self._pipe(remote_reader, writer)
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logging.error("Connection timeout")
|
||||
writer.write(b'HTTP/1.1 504 Gateway Timeout\r\n\r\n')
|
||||
await writer.drain()
|
||||
except Exception as e:
|
||||
logging.error(f"Error in CONNECT: {e}")
|
||||
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
|
||||
await writer.drain()
|
||||
|
||||
def _split_proxy_auth(self, proxy_addr):
|
||||
match = re.match(r'((?P<username>.+?):(?P<password>.+?)@)?(?P<host>.+)', proxy_addr)
|
||||
if match:
|
||||
username = match.group('username')
|
||||
password = match.group('password')
|
||||
host = match.group('host')
|
||||
if username and password:
|
||||
return f"{username}:{password}", host
|
||||
return None, proxy_addr
|
||||
|
||||
async def _pipe(self, reader, writer):
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
data = await reader.read(8192)
|
||||
if not data:
|
||||
break
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
async def _handle_request(self, method, path, headers, reader, writer):
|
||||
body = await reader.read()
|
||||
proxy = await self.get_next_proxy()
|
||||
proxy_type, proxy_addr = proxy.split('://')
|
||||
proxy_auth, proxy_host_port = self._split_proxy_auth(proxy_addr)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
limits=httpx.Limits(max_keepalive_connections=500, max_connections=3000),
|
||||
timeout=30
|
||||
) as client:
|
||||
try:
|
||||
if proxy_auth:
|
||||
proxy = f"{proxy_type}://{proxy_host_port}"
|
||||
headers['Proxy-Authorization'] = f'Basic {base64.b64encode(proxy_auth.encode()).decode()}'
|
||||
|
||||
async with client.stream(method, path, headers=headers, content=body, proxies=proxy) as response:
|
||||
await self._write_response(writer, response)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logging.error(f"请求处理出错: {e}")
|
||||
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
|
||||
await writer.drain()
|
||||
|
||||
async def _write_response(self, writer, response):
|
||||
try:
|
||||
writer.write(f'HTTP/1.1 {response.status_code} {response.reason_phrase}\r\n'.encode('utf-8', errors='ignore'))
|
||||
writer.write(b'Transfer-Encoding: chunked\r\n')
|
||||
for name, value in response.headers.items():
|
||||
if name.lower() != 'transfer-encoding':
|
||||
writer.write(f'{name}: {value}\r\n'.encode('utf-8', errors='ignore'))
|
||||
writer.write(b'\r\n')
|
||||
await writer.drain()
|
||||
|
||||
async for chunk in response.aiter_bytes(chunk_size=8192):
|
||||
if asyncio.current_task().cancelled():
|
||||
raise asyncio.CancelledError()
|
||||
writer.write(f'{len(chunk):X}\r\n'.encode('utf-8', errors='ignore'))
|
||||
writer.write(chunk)
|
||||
writer.write(b'\r\n')
|
||||
await writer.drain()
|
||||
writer.write(b'0\r\n\r\n')
|
||||
await writer.drain()
|
||||
except asyncio.CancelledError:
|
||||
logging.info("响应写入被取消")
|
||||
raise
|
||||
|
||||
def print_banner(config):
|
||||
auth_info = f"{config.get('username')}:{config.get('password')}" if config.get('username') and config.get('password') else "未设置 (无需认证)"
|
||||
banner_info = [
|
||||
('公众号', '樱花庄的本间白猫'),
|
||||
('博客', 'https://y.shironekosan.cn'),
|
||||
('Github', 'https://github.com/honmashironeko/ProxyCat'),
|
||||
('本机监听端口', config.get('port')),
|
||||
('代理轮换模式', '循环' if config.get('mode') == 'cycle' else '单轮'),
|
||||
('代理更换时间', f"{config.get('interval')}秒"),
|
||||
('默认账号密码', auth_info),
|
||||
]
|
||||
print(f"{Fore.MAGENTA}{'=' * 55}")
|
||||
for key, value in banner_info:
|
||||
print(f"{Fore.YELLOW}{key}: {Fore.GREEN}{value}")
|
||||
print(f"{Fore.MAGENTA}{'=' * 55}\n")
|
||||
|
||||
def update_status(server):
|
||||
while True:
|
||||
time_left = server.time_until_next_switch()
|
||||
status = f"\r{Fore.YELLOW}当前代理: {Fore.GREEN}{server.current_proxy} | {Fore.YELLOW}下次切换: {Fore.GREEN}{time_left:.1f}秒"
|
||||
print(status, end='', flush=True)
|
||||
time.sleep(1)
|
||||
|
||||
async def handle_client_wrapper(server, reader, writer, clients):
|
||||
task = asyncio.create_task(server.handle_client(reader, writer))
|
||||
clients.add(task)
|
||||
try:
|
||||
await task
|
||||
except Exception as e:
|
||||
logging.error(f"客户端处理出错: {e}")
|
||||
finally:
|
||||
clients.remove(task)
|
||||
|
||||
async def run_server(server):
|
||||
clients = set()
|
||||
server_instance = None
|
||||
try:
|
||||
server_instance = await asyncio.start_server(
|
||||
lambda r, w: handle_client_wrapper(server, r, w, clients),
|
||||
'0.0.0.0',
|
||||
server.config.getint('port', 1080)
|
||||
)
|
||||
async with server_instance:
|
||||
await server_instance.serve_forever()
|
||||
except asyncio.CancelledError:
|
||||
logging.info("服务器正在关闭...")
|
||||
finally:
|
||||
if server_instance:
|
||||
server_instance.close()
|
||||
await server_instance.wait_closed()
|
||||
for client in clients:
|
||||
client.cancel()
|
||||
await asyncio.gather(*[client for client in clients], return_exceptions=True)
|
||||
|
||||
async def check_for_updates():
|
||||
current_version = "ProxyCat-V1.4"
|
||||
timeout = 10
|
||||
try:
|
||||
async with AsyncClient() as client:
|
||||
try:
|
||||
response = await asyncio.wait_for(
|
||||
client.get("https://y.shironekosan.cn/1.html"),
|
||||
timeout=timeout
|
||||
)
|
||||
response.raise_for_status()
|
||||
content = response.text
|
||||
match = re.search(r'<p>(ProxyCat-V\d+\.\d+)</p>', content)
|
||||
if match:
|
||||
latest_version = match.group(1)
|
||||
if version.parse(latest_version.split('-V')[1]) > version.parse(current_version.split('-V')[1]):
|
||||
print(f"{Fore.YELLOW}发现新版本!当前版本: {current_version}, 最新版本: {latest_version}")
|
||||
print(f"{Fore.YELLOW}请访问 https://pan.quark.cn/s/39b4b5674570 获取最新版本。")
|
||||
print(f"{Fore.YELLOW}请访问 https://github.com/honmashironeko/ProxyCat 获取最新版本。")
|
||||
print(f"{Fore.YELLOW}请访问 https://pan.baidu.com/s/1C9LVC9aiaQeYFSj_2mWH1w?pwd=13r5 获取最新版本。")
|
||||
else:
|
||||
print(f"{Fore.GREEN}当前版本已是最新 ({current_version})")
|
||||
else:
|
||||
print(f"{Fore.RED}无法在响应中找到版本信息")
|
||||
except TimeoutException:
|
||||
print(f"{Fore.RED}检查更新超时,请检查网络连接")
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}检查更新时发生错误: {e}")
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}创建 HTTP 客户端时发生错误: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=logoprint.logos())
|
||||
parser.add_argument('-c', '--config', default='config.ini', help='配置文件路径')
|
||||
args = parser.parse_args()
|
||||
config = load_config(args.config)
|
||||
server = AsyncProxyServer(config)
|
||||
print_banner(config)
|
||||
asyncio.run(check_for_updates())
|
||||
status_thread = threading.Thread(target=update_status, args=(server,), daemon=True)
|
||||
status_thread.start()
|
||||
try:
|
||||
asyncio.run(run_server(server))
|
||||
except KeyboardInterrupt:
|
||||
logging.info("程序被用户中断")
|
||||
@@ -0,0 +1,23 @@
|
||||
[SETTINGS]
|
||||
# 本地服务器监听端口(默认为:1080)
|
||||
port = 1080
|
||||
|
||||
# 代理地址轮换模式:cycle 表示循环使用,once 表示用完即止(默认为:cycle)
|
||||
mode = cycle
|
||||
|
||||
# 代理地址更换时间(秒),设置为 0 时每次请求都更换 IP(默认为:300)
|
||||
interval = 5
|
||||
|
||||
# 本地服务器端口认证用户名((默认为:neko)当为空时不需要认证
|
||||
username = neko
|
||||
|
||||
# 本地服务器端口认证密码(默认为:123456)当为空时不需要认证
|
||||
password = 123456
|
||||
|
||||
# 是否使用 getip 模块获取代理地址 True or False(默认为:False)
|
||||
use_getip = False
|
||||
|
||||
# 代理地址列表文件(默认为:ip.txt)
|
||||
proxy_file = ip.txt
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import requests
|
||||
|
||||
def newip():
|
||||
print("正在获取新的代理IP")
|
||||
url = ""
|
||||
url = f""
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
newip = "socks5://"+response.text.split("\n\r")[0]
|
||||
@@ -0,0 +1 @@
|
||||
socks5://127.0.0.1:7890
|
||||
@@ -0,0 +1,4 @@
|
||||
requests
|
||||
pysocks
|
||||
colorama
|
||||
httpx
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,373 @@
|
||||
from httpx import AsyncClient, TimeoutException
|
||||
from colorama import init, Fore
|
||||
from packaging import version
|
||||
from itertools import cycle
|
||||
import configparser
|
||||
import threading
|
||||
import logoprint
|
||||
import argparse
|
||||
import logging
|
||||
import asyncio
|
||||
import socket
|
||||
import base64
|
||||
import getip
|
||||
import httpx
|
||||
import time
|
||||
import re
|
||||
|
||||
init(autoreset=True)
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
def load_config(config_file='config.ini'):
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_file, encoding='utf-8')
|
||||
return config['SETTINGS']
|
||||
|
||||
def load_proxies(file_path='ip.txt'):
|
||||
with open(file_path, 'r') as file:
|
||||
return [line.strip() for line in file if '://' in line]
|
||||
|
||||
class AsyncProxyServer:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.username = config.get('username', '').strip()
|
||||
self.password = config.get('password', '').strip()
|
||||
self.auth_required = bool(self.username and self.password)
|
||||
self.mode = config.get('mode', 'cycle')
|
||||
self.interval = config.getint('interval', 300)
|
||||
self.use_getip = config.getboolean('use_getip', False)
|
||||
self.proxy_file = config.get('proxy_file', 'ip.txt')
|
||||
self.proxies = self.load_proxies()
|
||||
self.initial_proxy = self.proxies[0] if self.proxies else "No proxy available"
|
||||
self.proxy_cycle = cycle(self.proxies)
|
||||
self.current_proxy = self.initial_proxy
|
||||
self.last_switch_time = time.time()
|
||||
self.rate_limiter = asyncio.Queue(maxsize=3000)
|
||||
|
||||
def load_proxies(self):
|
||||
if self.use_getip:
|
||||
return [getip.newip()]
|
||||
else:
|
||||
return load_proxies(self.proxy_file)
|
||||
|
||||
async def get_next_proxy(self):
|
||||
current_time = time.time()
|
||||
if current_time - self.last_switch_time >= self.interval:
|
||||
if self.use_getip:
|
||||
self.current_proxy = getip.newip()
|
||||
else:
|
||||
self.current_proxy = next(self.proxy_cycle)
|
||||
self.last_switch_time = current_time
|
||||
logging.info(f"Switch to a new proxy: {self.current_proxy}")
|
||||
return self.current_proxy
|
||||
|
||||
def time_until_next_switch(self):
|
||||
return max(0, self.interval - (time.time() - self.last_switch_time))
|
||||
|
||||
async def acquire(self):
|
||||
await self.rate_limiter.put(None)
|
||||
await asyncio.sleep(0.001)
|
||||
self.rate_limiter.get_nowait()
|
||||
|
||||
async def handle_client(self, reader, writer):
|
||||
try:
|
||||
await asyncio.shield(self._handle_client_impl(reader, writer))
|
||||
except asyncio.CancelledError:
|
||||
logging.info("Client process canceled")
|
||||
except Exception as e:
|
||||
logging.error(f"Client processing error: {e}")
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
async def _handle_client_impl(self, reader, writer):
|
||||
try:
|
||||
await self.acquire()
|
||||
request_line = await reader.readline()
|
||||
if not request_line:
|
||||
return
|
||||
|
||||
try:
|
||||
method, path, _ = request_line.decode('utf-8', errors='ignore').split()
|
||||
except ValueError:
|
||||
logging.error(f"Invalid request line: {request_line}")
|
||||
return
|
||||
|
||||
headers = {}
|
||||
while True:
|
||||
line = await reader.readline()
|
||||
if line == b'\r\n':
|
||||
break
|
||||
if line == b'':
|
||||
return
|
||||
try:
|
||||
name, value = line.decode('utf-8', errors='ignore').strip().split(': ', 1)
|
||||
headers[name.lower()] = value
|
||||
except ValueError:
|
||||
logging.error(f"Invalid request line: {line}")
|
||||
continue
|
||||
|
||||
if self.auth_required and not self._authenticate(headers):
|
||||
writer.write(b'HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="Proxy"\r\n\r\n')
|
||||
await writer.drain()
|
||||
return
|
||||
|
||||
if method == 'CONNECT':
|
||||
await self._handle_connect(path, reader, writer)
|
||||
else:
|
||||
await self._handle_request(method, path, headers, reader, writer)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logging.error(f"An error occurred while processing the client request: {e}")
|
||||
|
||||
def _authenticate(self, headers):
|
||||
if not self.auth_required:
|
||||
return True
|
||||
|
||||
auth = headers.get('proxy-authorization')
|
||||
if not auth:
|
||||
return False
|
||||
try:
|
||||
scheme, credentials = auth.split()
|
||||
if scheme.lower() != 'basic':
|
||||
return False
|
||||
username, password = base64.b64decode(credentials).decode().split(':')
|
||||
return username == self.username and password == self.password
|
||||
except:
|
||||
return False
|
||||
|
||||
async def _handle_connect(self, path, reader, writer):
|
||||
try:
|
||||
host, port = path.split(':')
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
logging.error(f"Invalid CONNECT path: {path}")
|
||||
writer.write(b'HTTP/1.1 400 Bad Request\r\n\r\n')
|
||||
await writer.drain()
|
||||
return
|
||||
|
||||
proxy = await self.get_next_proxy()
|
||||
proxy_type, proxy_addr = proxy.split('://')
|
||||
proxy_auth, proxy_host_port = self._split_proxy_auth(proxy_addr)
|
||||
proxy_host, proxy_port = proxy_host_port.split(':')
|
||||
proxy_port = int(proxy_port)
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
remote_reader, remote_writer = await asyncio.open_connection(proxy_host, proxy_port)
|
||||
|
||||
if proxy_type == 'http':
|
||||
connect_headers = [f'CONNECT {host}:{port} HTTP/1.1', f'Host: {host}:{port}']
|
||||
if proxy_auth:
|
||||
auth_header = f'Proxy-Authorization: Basic {base64.b64encode(proxy_auth.encode()).decode()}'
|
||||
connect_headers.append(auth_header)
|
||||
connect_request = '\r\n'.join(connect_headers) + '\r\n\r\n'
|
||||
remote_writer.write(connect_request.encode())
|
||||
await remote_writer.drain()
|
||||
response = await remote_reader.readline()
|
||||
if not response.startswith(b'HTTP/1.1 200'):
|
||||
raise Exception("Bad Gateway")
|
||||
while (await remote_reader.readline()) != b'\r\n':
|
||||
pass
|
||||
elif proxy_type in ['socks4', 'socks5']:
|
||||
if proxy_type == 'socks4':
|
||||
remote_writer.write(b'\x04\x01' + port.to_bytes(2, 'big') + socket.inet_aton(host) + b'\x00')
|
||||
else:
|
||||
remote_writer.write(b'\x05\x01\x00')
|
||||
await remote_writer.drain()
|
||||
if (await remote_reader.read(2))[1] == 0:
|
||||
remote_writer.write(b'\x05\x01\x00\x03' + len(host).to_bytes(1, 'big') + host.encode() + port.to_bytes(2, 'big'))
|
||||
await remote_writer.drain()
|
||||
if (await remote_reader.read(10))[1] != 0:
|
||||
raise Exception("Bad Gateway")
|
||||
else:
|
||||
raise Exception("Unsupported proxy type")
|
||||
|
||||
writer.write(b'HTTP/1.1 200 Connection Established\r\n\r\n')
|
||||
await writer.drain()
|
||||
|
||||
await asyncio.gather(
|
||||
self._pipe(reader, remote_writer),
|
||||
self._pipe(remote_reader, writer)
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logging.error("Connection timeout")
|
||||
writer.write(b'HTTP/1.1 504 Gateway Timeout\r\n\r\n')
|
||||
await writer.drain()
|
||||
except Exception as e:
|
||||
logging.error(f"Error in CONNECT: {e}")
|
||||
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
|
||||
await writer.drain()
|
||||
|
||||
def _split_proxy_auth(self, proxy_addr):
|
||||
match = re.match(r'((?P<username>.+?):(?P<password>.+?)@)?(?P<host>.+)', proxy_addr)
|
||||
if match:
|
||||
username = match.group('username')
|
||||
password = match.group('password')
|
||||
host = match.group('host')
|
||||
if username and password:
|
||||
return f"{username}:{password}", host
|
||||
return None, proxy_addr
|
||||
|
||||
async def _pipe(self, reader, writer):
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
data = await reader.read(8192)
|
||||
if not data:
|
||||
break
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
async def _handle_request(self, method, path, headers, reader, writer):
|
||||
body = await reader.read()
|
||||
proxy = await self.get_next_proxy()
|
||||
proxy_type, proxy_addr = proxy.split('://')
|
||||
proxy_auth, proxy_host_port = self._split_proxy_auth(proxy_addr)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
limits=httpx.Limits(max_keepalive_connections=500, max_connections=3000),
|
||||
timeout=30
|
||||
) as client:
|
||||
try:
|
||||
if proxy_auth:
|
||||
proxy = f"{proxy_type}://{proxy_host_port}"
|
||||
headers['Proxy-Authorization'] = f'Basic {base64.b64encode(proxy_auth.encode()).decode()}'
|
||||
|
||||
async with client.stream(method, path, headers=headers, content=body, proxies=proxy) as response:
|
||||
await self._write_response(writer, response)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logging.error(f"Request processing error: {e}")
|
||||
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
|
||||
await writer.drain()
|
||||
|
||||
async def _write_response(self, writer, response):
|
||||
try:
|
||||
writer.write(f'HTTP/1.1 {response.status_code} {response.reason_phrase}\r\n'.encode('utf-8', errors='ignore'))
|
||||
writer.write(b'Transfer-Encoding: chunked\r\n')
|
||||
for name, value in response.headers.items():
|
||||
if name.lower() != 'transfer-encoding':
|
||||
writer.write(f'{name}: {value}\r\n'.encode('utf-8', errors='ignore'))
|
||||
writer.write(b'\r\n')
|
||||
await writer.drain()
|
||||
|
||||
async for chunk in response.aiter_bytes(chunk_size=8192):
|
||||
if asyncio.current_task().cancelled():
|
||||
raise asyncio.CancelledError()
|
||||
writer.write(f'{len(chunk):X}\r\n'.encode('utf-8', errors='ignore'))
|
||||
writer.write(chunk)
|
||||
writer.write(b'\r\n')
|
||||
await writer.drain()
|
||||
writer.write(b'0\r\n\r\n')
|
||||
await writer.drain()
|
||||
except asyncio.CancelledError:
|
||||
logging.info("Response write canceled")
|
||||
raise
|
||||
|
||||
def print_banner(config):
|
||||
auth_info = f"{config.get('username')}:{config.get('password')}" if config.get('username') and config.get('password') else "Not set (no authentication required)"
|
||||
banner_info = [
|
||||
('公众号', '樱花庄的本间白猫'),
|
||||
('Blog', 'https://y.shironekosan.cn'),
|
||||
('Github', 'https://github.com/honmashironeko/ProxyCat'),
|
||||
('Local listening port', config.get('port')),
|
||||
('Proxy rotation mode', 'cycle' if config.get('mode') == 'cycle' else 'once'),
|
||||
('Agent change time', f"{config.get('interval')}秒"),
|
||||
('Default account password', auth_info),
|
||||
]
|
||||
print(f"{Fore.MAGENTA}{'=' * 55}")
|
||||
for key, value in banner_info:
|
||||
print(f"{Fore.YELLOW}{key}: {Fore.GREEN}{value}")
|
||||
print(f"{Fore.MAGENTA}{'=' * 55}\n")
|
||||
|
||||
def update_status(server):
|
||||
while True:
|
||||
time_left = server.time_until_next_switch()
|
||||
status = f"\r{Fore.YELLOW}Current Proxy: {Fore.GREEN}{server.current_proxy} | {Fore.YELLOW}Next switch: {Fore.GREEN}{time_left:.1f}秒"
|
||||
print(status, end='', flush=True)
|
||||
time.sleep(1)
|
||||
|
||||
async def handle_client_wrapper(server, reader, writer, clients):
|
||||
task = asyncio.create_task(server.handle_client(reader, writer))
|
||||
clients.add(task)
|
||||
try:
|
||||
await task
|
||||
except Exception as e:
|
||||
logging.error(f"Client processing error: {e}")
|
||||
finally:
|
||||
clients.remove(task)
|
||||
|
||||
async def run_server(server):
|
||||
clients = set()
|
||||
server_instance = None
|
||||
try:
|
||||
server_instance = await asyncio.start_server(
|
||||
lambda r, w: handle_client_wrapper(server, r, w, clients),
|
||||
'0.0.0.0',
|
||||
server.config.getint('port', 1080)
|
||||
)
|
||||
async with server_instance:
|
||||
await server_instance.serve_forever()
|
||||
except asyncio.CancelledError:
|
||||
logging.info("The server is shutting down...")
|
||||
finally:
|
||||
if server_instance:
|
||||
server_instance.close()
|
||||
await server_instance.wait_closed()
|
||||
for client in clients:
|
||||
client.cancel()
|
||||
await asyncio.gather(*[client for client in clients], return_exceptions=True)
|
||||
|
||||
async def check_for_updates():
|
||||
current_version = "ProxyCat-V1.4"
|
||||
timeout = 10
|
||||
try:
|
||||
async with AsyncClient() as client:
|
||||
try:
|
||||
response = await asyncio.wait_for(
|
||||
client.get("https://y.shironekosan.cn/1.html"),
|
||||
timeout=timeout
|
||||
)
|
||||
response.raise_for_status()
|
||||
content = response.text
|
||||
match = re.search(r'<p>(ProxyCat-V\d+\.\d+)</p>', content)
|
||||
if match:
|
||||
latest_version = match.group(1)
|
||||
if version.parse(latest_version.split('-V')[1]) > version.parse(current_version.split('-V')[1]):
|
||||
print(f"{Fore.YELLOW}New version found! Current version: {current_version}, Latest version: {latest_version}")
|
||||
print(f"{Fore.YELLOW}Please visit https://pan.quark.cn/s/39b4b5674570 to get the latest version")
|
||||
print(f"{Fore.YELLOW}Please visit https://github.com/honmashironeko/ProxyCat to get the latest version")
|
||||
print(f"{Fore.YELLOW}Please visit https://pan.baidu.com/s/1C9LVC9aiaQeYFSj_2mWH1w?pwd=13r5 to get the latest version")
|
||||
else:
|
||||
print(f"{Fore.GREEN}The current version is the latest ({current_version})")
|
||||
else:
|
||||
print(f"{Fore.RED}Unable to find version information in the response")
|
||||
except TimeoutException:
|
||||
print(f"{Fore.RED}Checking for updates has timed out, please check your network connection")
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}An error occurred while checking for updates: {e}")
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}An error occurred while creating the HTTP client: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=logoprint.logos())
|
||||
parser.add_argument('-c', '--config', default='config.ini', help='Configuration file path')
|
||||
args = parser.parse_args()
|
||||
config = load_config(args.config)
|
||||
server = AsyncProxyServer(config)
|
||||
print_banner(config)
|
||||
asyncio.run(check_for_updates())
|
||||
status_thread = threading.Thread(target=update_status, args=(server,), daemon=True)
|
||||
status_thread.start()
|
||||
try:
|
||||
asyncio.run(run_server(server))
|
||||
except KeyboardInterrupt:
|
||||
logging.info("The program was interrupted by the user")
|
||||
@@ -0,0 +1,211 @@
|
||||

|
||||
|
||||
[English](./ProxyCat-EN/README-EN.md) | [中文](./README.md)
|
||||
|
||||
## 项目概述
|
||||
|
||||
渗透过程中总是遇到需要隐藏IP地址或者更换IP地址绕过安全设备的情况,但是市面上的隧道代理价格极其昂贵,普遍在20-40元/天,这高昂的费用令人难以接受。但是笔者注意到短效IP其实性价比很高,一个IP只需要几分钱,平均只需要0.2-3元/天。
|
||||
|
||||
综上所述,本工具应运而生!目的是在于将持续时间仅有1分钟-60分钟不等的短效IP转变成一个固定IP供其他工具使用,形成代理池服务器,部署一次即可永久使用。
|
||||
|
||||

|
||||
|
||||
## 支持的功能
|
||||
|
||||
```
|
||||
本地监听服务类:自定义监听端口、自定义监听端口身份鉴别
|
||||
代理地址类:4种协议支持、代理地址定时更换、代理地址每次请求后更换、API接口自动获取代理地址、支持有身份鉴别的代理地址
|
||||
服务运行时:支持仅在收到请求后才更换代理地址、支持超高并发、支持HTTP、HTTPS请求协议、支持每次启动检查版本更新
|
||||
系统兼容性:Windows、Linux、Mac原则上全支持、支持VPS远程部署、本地部署
|
||||
```
|
||||
|
||||
## 项目使用
|
||||
|
||||
### 安装依赖
|
||||
|
||||
工具基于 Python 实现(建议使用python3.8以上版本),在使用前请使用以下命令配置依赖:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
# or 推荐使用国内源:
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
|
||||
```
|
||||
|
||||
使用 `python` 命令在项目目录运行 `python3 ProxyCat.py -h` 回显如下即配置成功:
|
||||
|
||||
```
|
||||
# python3 ProxyCat.py -h
|
||||
|\ _,,,---,,_ by 本间白猫
|
||||
ZZZzz /,`.-'`' -. ;-;;,_
|
||||
|,4- ) )-,_. ,\ ( `'-'
|
||||
'---''(_/--' `-'\_) ProxyCat
|
||||
|
||||
用法: ProxyCat.py [-h] [-c]
|
||||
|
||||
参数:
|
||||
-h, --help 显示此帮助信息并退出
|
||||
-c C 指定配置文件名(默认config.ini)
|
||||
```
|
||||
|
||||
### ip.txt 手动录入代理地址
|
||||
|
||||
按照该格式`socks5://127.0.0.1:7890` or` http://127.0.0.1:7890` 一行一个填入ip.txt文件中:
|
||||
|
||||
```ip.txt
|
||||
socks5://127.0.0.1:7890
|
||||
socks4://127.0.0.1:7890
|
||||
https://127.0.0.1:7890
|
||||
http://127.0.0.1:7890
|
||||
...
|
||||
```
|
||||
|
||||
> 如果您缺少合适的国内代理地址,恰巧又有几块钱的话可以联系作者(微信公众号:樱花庄的本间白猫),获取多家低价代理购买地址,为作者带来几毛钱的推荐收益~
|
||||
|
||||
在config.ini(或自定义配置文件)中配置参数:
|
||||
|
||||
```
|
||||
[SETTINGS]
|
||||
# 本地服务器监听端口(默认为:1080)
|
||||
port = 1080
|
||||
|
||||
# 代理地址轮换模式:cycle 表示循环使用,once 表示用完即止(默认为:cycle)
|
||||
mode = cycle
|
||||
|
||||
# 代理地址更换时间(秒),设置为 0 时每次请求都更换 IP(默认为:300)
|
||||
interval = 300
|
||||
|
||||
# 本地服务器端口认证用户名((默认为:neko)当为空时不需要认证
|
||||
username = neko
|
||||
|
||||
# 本地服务器端口认证密码(默认为:123456)当为空时不需要认证
|
||||
password = 123456
|
||||
|
||||
# 是否使用 getip 模块获取代理地址 True or False(默认为:False)
|
||||
use_getip = False
|
||||
|
||||
# 代理地址列表文件(默认为:ip.txt)
|
||||
proxy_file = ip.txt
|
||||
```
|
||||
|
||||
配置对应参数后即可使用:
|
||||
|
||||
```bash
|
||||
# python3 ProxyCat.py
|
||||
```
|
||||
|
||||
**演示效果如下**
|
||||
|
||||
固定代理地址(默认):http://neko:[email protected]:1080 or http://127.0.0.1:1080
|
||||
|
||||
如果您是部署在公网,将127.0.0.1替换成您的公网IP即可。
|
||||
|
||||

|
||||
|
||||
### 使用接口自动获取代理地址
|
||||
|
||||
工具支持直接调用代理地址获取的API接口,当您配置`use_getip = True`时,工具将不再从本地ip.txt中读取代理地址,而是更换为通过执行 **getip.py** 脚本来获取新的代理地址。
|
||||
|
||||
此时,您需要将 **getip.py** 内容修改为您自己的接口,格式为 `IP:PORT`,默认为 `socks5` 协议,如果您要使用 `http` ,请手动更改。
|
||||
|
||||
**演示效果如下**
|
||||
|
||||
> 此处的运营商可在下方广告区域获取
|
||||
|
||||

|
||||
|
||||
## 性能表现
|
||||
|
||||
当前经过实际测试,在代理地址服务器性能足够的情况下,能够做到500并发不丢包,基本可以覆盖大部分扫描和渗透测试。
|
||||
|
||||

|
||||
|
||||
## 免责申明
|
||||
|
||||
- 如果您下载、安装、使用、修改本工具及相关代码,即表明您信任本工具
|
||||
- 在使用本工具时造成对您自己或他人任何形式的损失和伤害,我们不承担任何责任
|
||||
- 如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任
|
||||
- 请您务必审慎阅读、充分理解各条款内容,特别是免除或者限制责任的条款,并选择接受或不接受
|
||||
- 除非您已阅读并接受本协议所有条款,否则您无权下载、安装或使用本工具
|
||||
- 您的下载、安装、使用等行为即视为您已阅读并同意上述协议的约束
|
||||
|
||||
## 更新日志
|
||||
|
||||
**2024/08/31**
|
||||
|
||||
- 项目大结构调整
|
||||
- 美化显示,将持续提示下一次更换代理地址的时间
|
||||
- 已支持`ctrl+c`停止运行
|
||||
- 大幅度调整为异步请求,并发效率提升,实测1000并发,共5000包,丢50包左右约99%稳定性,500并发0丢包
|
||||
- 不再采取运行时指定参数方案,修改为从本地ini配置文件中读取,易用性更高
|
||||
- 支持本地无认证,适配更多软件代理方式
|
||||
- 增加版本检测功能,将自动提示版本信息
|
||||
- 增加代理服务器地址的身份鉴别功能,但仅支持本地读取,因大多数API都需要白名单,因此不重复提供
|
||||
- 增加功能,仅在收到新请求的情况下才使用getip更新,减少IP消耗
|
||||
- 增加自识别代理服务器地址协议,以适配更多代理商
|
||||
- 增加支持HTTPS、socks4代理协议,目前已覆盖HTTP、HTTPS、socks5、socks4协议
|
||||
|
||||
**2024/08/25**
|
||||
|
||||
- 读取ip.txt时自动跳过空行
|
||||
- httpx更换为并发池,提高性能
|
||||
- 增加缓冲字典,相同站点降低延迟
|
||||
- 每次请求更换IP逻辑修改为,随机选择代理
|
||||
- 采用更加高效的结构和算法,优化请求处理逻辑
|
||||
|
||||
**2024/08/24**
|
||||
|
||||
- 采用异步方案提高并发能力和减少超时
|
||||
- 重复代码封装提高代码复用性
|
||||
|
||||
**2024/08/23**
|
||||
|
||||
- 修改并发逻辑
|
||||
- 增加身份鉴别功能
|
||||
- 增加IP获取接口,永久更换IP
|
||||
- 增加每次请求更换IP功能
|
||||
|
||||
## 开发计划
|
||||
|
||||
- [x] 增加本地服务器身份鉴别功能,保证在公网使用过程中不被恶意盗用
|
||||
|
||||
- [x] 增加每次请求更换IP功能
|
||||
|
||||
- [x] 增加静态代理自动获取更新模块,从而永久运行
|
||||
|
||||
- [ ] 增加负载均衡模式,同时使用大量代理地址发送,从而有效提高并发效率,减少单一服务器负载
|
||||
|
||||
- [x] 增加版本检测功能
|
||||
|
||||
- [x] 增加代理地址身份鉴别支持
|
||||
|
||||
- [x] 增加功能,仅在收到新请求的情况下才使用getip更新,减少IP消耗
|
||||
|
||||
- [ ] 首次启动时批量对ip.txt中的代理服务器进行有效性检查
|
||||
|
||||
如果您有好的idea,或者使用过程中遇到的bug,都请辛苦您添加作者联系方式进行反馈!
|
||||
|
||||
微信公众号:樱花庄的本间白猫
|
||||
|
||||
## 鸣谢
|
||||
|
||||
本排名不分先后,感谢为本项目提供帮助的师傅们。
|
||||
|
||||
[AabyssZG (曾哥)](https://github.com/AabyssZG)
|
||||
|
||||
[ProbiusOfficial (探姬)](https://github.com/ProbiusOfficial)
|
||||
|
||||
[](https://star-history.com/#honmashironeko/ProxyCat&Date)
|
||||
|
||||
## 赞助开源
|
||||
|
||||
开源不易,如果您觉得工具不错,或许可以试着赞助一下作者的开发哦~
|
||||
|
||||

|
||||
|
||||
## 代理推荐
|
||||
|
||||
- [第一家便宜大碗代理购买,用邀请码注册得5000免费IP+10元优惠券](https://h.shanchendaili.com/invite_reg.html?invite=fM6fVG)
|
||||
- [各大运营商流量卡](https://172.lot-ml.com/ProductEn/Index/0b7c9adef5e9648f)
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 86 KiB |
@@ -0,0 +1,22 @@
|
||||
[SETTINGS]
|
||||
# Local server listening port (default: 1080)
|
||||
port = 1080
|
||||
|
||||
# Proxy address rotation mode: cycle means cyclic use, once means once used up (default is: cycle)
|
||||
mode = cycle
|
||||
|
||||
# Proxy address change time (seconds), when set to 0, the IP will be changed for each request (default: 300)
|
||||
interval = 300
|
||||
|
||||
# Local server port authentication username (default: neko) No authentication is required when it is empty
|
||||
username = neko
|
||||
|
||||
# Local server port authentication password (default: 123456) No authentication is required when it is empty
|
||||
password = 123456
|
||||
|
||||
# Whether to use the getip module to obtain the proxy address True or False (default: False)
|
||||
use_getip = False
|
||||
|
||||
# Proxy address list file (default: ip.txt)
|
||||
proxy_file = ip.txt
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import requests
|
||||
|
||||
def newip():
|
||||
print("Getting new proxy IP")
|
||||
url = f""
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
newip = "socks5://"+response.text.split("\n\r")[0]
|
||||
print("The new proxy IP is:"+newip)
|
||||
return newip
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
socks5://127.0.0.1:7890
|
||||
@@ -0,0 +1,58 @@
|
||||
import random
|
||||
|
||||
logo1 = """
|
||||
|\ _,,,---,,_ by 本间白猫
|
||||
ZZZzz /,`.-'`' -. ;-;;,_
|
||||
|,4- ) )-,_. ,\ ( `'-'
|
||||
'---''(_/--' `-'\_) ProxyCat
|
||||
"""
|
||||
logo2 = """
|
||||
* ,MMM8&&&. *
|
||||
MMMM88&&&&& .
|
||||
MMMM88&&&&&&&
|
||||
* MMM88&&&&&&&&
|
||||
MMM88&&&&&&&&
|
||||
'MMM88&&&&&&'
|
||||
'MMM8&&&' *
|
||||
/\/|_ __/\\
|
||||
/ -\ /- ~\ . '
|
||||
\ =_YT_ = /
|
||||
/==*(` `\ ~ \ ProxyCat
|
||||
/ \ / `\ by 本间白猫
|
||||
| | ) ~ (
|
||||
/ \ / ~ \\
|
||||
\ / \~ ~/
|
||||
_/\_/\_/\__ _/_/\_/\__~__/_/\_/\_/\_/\_/\_
|
||||
| | | | ) ) | | | (( | | | | | |
|
||||
| | | |( ( | | | \\ | | | | | |
|
||||
| | | | )_) | | | |))| | | | | |
|
||||
| | | | | | | | (/ | | | | | |
|
||||
| | | | | | | | | | | | | | |
|
||||
"""
|
||||
logo3 = """
|
||||
/\_/\ _
|
||||
/`` \ / )
|
||||
|n n |__ ( (
|
||||
=(Y =.'` `\ \ \\
|
||||
{`"` \ ) )
|
||||
{ / |/ /
|
||||
\\ ,( / /
|
||||
ProxyCat) ) /-'\ ,_.' by 本间白猫
|
||||
(,(,/ ((,,/
|
||||
"""
|
||||
logo4 = """
|
||||
.-o=o-.
|
||||
, /=o=o=o=\ .--.
|
||||
_|\|=o=O=o=O=| \\
|
||||
__.' a`\=o=o=o=(`\ /
|
||||
'. a 4/`|.-""'`\ \ ;'`) .---.
|
||||
\ .' / .--' |_.' / .-._)
|
||||
by 本间白猫 `) _.' / /`-.__.' /
|
||||
ProxyCat `'-.____; /'-.___.-'
|
||||
`\"""`
|
||||
"""
|
||||
|
||||
logos_list = [logo1, logo2, logo3, logo4]
|
||||
def logos():
|
||||
selected_logo = random.choice(logos_list)
|
||||
print(selected_logo)
|
||||
@@ -0,0 +1,4 @@
|
||||
requests
|
||||
pysocks
|
||||
colorama
|
||||
httpx
|
||||
@@ -1,292 +0,0 @@
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from httpx import AsyncClient, Limits
|
||||
import multiprocessing
|
||||
import threading
|
||||
import logoprint
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
import select
|
||||
import base64
|
||||
import getip
|
||||
import socks
|
||||
import httpx
|
||||
import time
|
||||
import random
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
proxy_index, rotate_mode, rotate_interval = 0, 'cycle', 60
|
||||
proxy_fail_count = {}
|
||||
cache = {}
|
||||
|
||||
def load_proxies(file_path='ip.txt'):
|
||||
with open(file_path, 'r') as file:
|
||||
proxies = [line.strip().split('://') for line in file]
|
||||
return [(p[0], *p[1].split(':')) for p in proxies if len(p) == 2]
|
||||
|
||||
def rotate_proxies(proxies, interval):
|
||||
global proxy_index
|
||||
while True:
|
||||
time.sleep(interval)
|
||||
if args.k:
|
||||
proxies.clear()
|
||||
proxies.extend(get_proxy_from_getip())
|
||||
proxy_index = (proxy_index + 1) % len(proxies)
|
||||
else:
|
||||
if rotate_mode == 'cycle':
|
||||
proxy_index = (proxy_index + 1) % len(proxies)
|
||||
elif rotate_mode == 'once' and proxy_index < len(proxies) - 1:
|
||||
proxy_index += 1
|
||||
logging.info(f"切换到代理地址: {proxies[proxy_index]}")
|
||||
|
||||
def get_proxy_from_getip():
|
||||
proxy = getip.newip()
|
||||
protocol, host_port = proxy.split('://')
|
||||
host, port = host_port.split(':')
|
||||
return [(protocol, host, port)]
|
||||
|
||||
proxies = []
|
||||
|
||||
class ProxyHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.client = AsyncClient(http2=True, timeout=httpx.Timeout(30.0, read=30.0), limits=Limits(max_connections=500))
|
||||
self.tunnel_established = False
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _update_proxy(self):
|
||||
global proxy_index, proxy_fail_count
|
||||
if rotate_interval == 0:
|
||||
if args.k:
|
||||
proxies.clear()
|
||||
proxies.extend(get_proxy_from_getip())
|
||||
proxy_index = random.randint(0, len(proxies) - 1)
|
||||
logging.info(f"切换到代理地址: {proxies[proxy_index]}")
|
||||
protocol, host, port = proxies[proxy_index]
|
||||
self.proxy_dict = {"http://": f"{protocol}://{host}:{port}", "https://": f"{protocol}://{host}:{port}"}
|
||||
|
||||
def _authenticate(self):
|
||||
auth = self.headers.get('Proxy-Authorization')
|
||||
if not auth:
|
||||
self.send_response(407)
|
||||
self.send_header('Proxy-Authenticate', 'Basic realm="Proxy"')
|
||||
self.end_headers()
|
||||
return False
|
||||
auth = auth.split()
|
||||
if len(auth) != 2:
|
||||
self.send_error(400, message="Invalid authentication format")
|
||||
return False
|
||||
if auth[0].lower() != 'basic':
|
||||
self.send_error(400, message="Unsupported authentication method")
|
||||
return False
|
||||
auth = auth[1].encode('utf-8')
|
||||
auth = base64.b64decode(auth).decode('utf-8')
|
||||
username, password = auth.split(':', 1)
|
||||
if username != self.server.username or password != self.server.password:
|
||||
self.send_error(403, message="Invalid credentials")
|
||||
return False
|
||||
return True
|
||||
|
||||
def do_GET(self):
|
||||
if self._authenticate():
|
||||
self._proxy_request()
|
||||
|
||||
def do_POST(self):
|
||||
if self._authenticate():
|
||||
self._proxy_request()
|
||||
|
||||
def do_CONNECT(self):
|
||||
if self._authenticate():
|
||||
self._tunnel_request()
|
||||
|
||||
def _proxy_request(self):
|
||||
self._update_proxy()
|
||||
data = self.rfile.read(int(self.headers['Content-Length'])) if 'Content-Length' in self.headers else None
|
||||
headers = {key: val for key, val in self.headers.items()}
|
||||
headers['Connection'] = 'keep-alive'
|
||||
|
||||
async def handle():
|
||||
for attempt in range(3):
|
||||
try:
|
||||
logging.info(f"处理请求: {self.command} {self.path}")
|
||||
response = await self.client.request(self.command, self.path, headers=headers, data=data,
|
||||
proxies=self.proxy_dict, stream=True)
|
||||
self.send_response(response.status_code)
|
||||
self.send_headers(response)
|
||||
await self.forward_content(response)
|
||||
break
|
||||
except (httpx.RequestError, socket.timeout, OSError) as e:
|
||||
logging.error(f"请求失败: {e}")
|
||||
self._handle_proxy_failure()
|
||||
if attempt == 2:
|
||||
self.send_error(500, message=str(e))
|
||||
|
||||
asyncio.run(handle())
|
||||
|
||||
def _handle_proxy_failure(self):
|
||||
global proxy_index, proxy_fail_count
|
||||
current_proxy = proxies[proxy_index]
|
||||
if current_proxy in proxy_fail_count:
|
||||
proxy_fail_count[current_proxy] += 1
|
||||
else:
|
||||
proxy_fail_count[current_proxy] = 1
|
||||
|
||||
if proxy_fail_count[current_proxy] >= 3:
|
||||
logging.info(f"代理地址 {current_proxy} 失败次数达到3次,尝试切换到下一个代理")
|
||||
original_proxy_index = proxy_index
|
||||
if args.k:
|
||||
new_proxies = get_proxy_from_getip()
|
||||
if new_proxies:
|
||||
proxies.clear()
|
||||
proxies.extend(new_proxies)
|
||||
proxy_index = random.randint(0, len(proxies) - 1)
|
||||
logging.info(f"切换到新代理地址: {proxies[proxy_index]}")
|
||||
else:
|
||||
logging.warning("无法获取新代理,继续使用原代理")
|
||||
proxy_index = original_proxy_index
|
||||
else:
|
||||
if rotate_mode == 'cycle':
|
||||
proxy_index = random.randint(0, len(proxies) - 1)
|
||||
elif rotate_mode == 'once' and proxy_index < len(proxies) - 1:
|
||||
proxy_index += 1
|
||||
logging.info(f"切换到代理地址: {proxies[proxy_index]}")
|
||||
proxy_fail_count[current_proxy] = 0
|
||||
|
||||
def send_headers(self, response):
|
||||
for key, value in response.headers.items():
|
||||
if key.lower() != 'connection':
|
||||
self.send_header(key, value)
|
||||
self.send_header('Connection', 'keep-alive')
|
||||
self.end_headers()
|
||||
|
||||
async def forward_content(self, response):
|
||||
async for chunk in response.aiter_bytes(chunk_size=4096):
|
||||
if chunk:
|
||||
self.wfile.write(chunk)
|
||||
self.wfile.flush()
|
||||
|
||||
def _tunnel_request(self):
|
||||
self._update_proxy()
|
||||
host, port = self.path.split(':')
|
||||
try:
|
||||
if not self.tunnel_established:
|
||||
remote_socket = self._connect_via_proxy(host, int(port))
|
||||
self.tunnel_established = True
|
||||
self.send_response(200, 'Connection Established')
|
||||
self.send_header('Connection', 'keep-alive')
|
||||
self.end_headers()
|
||||
self._forward_data(self.connection, remote_socket)
|
||||
else:
|
||||
self.send_response(200, 'Connection Already Established')
|
||||
self.send_header('Connection', 'keep-alive')
|
||||
self.end_headers()
|
||||
except (socket.error, Exception) as e:
|
||||
logging.error(f"隧道请求失败: {e}")
|
||||
self._handle_proxy_failure()
|
||||
self.send_error(502, message=str(e))
|
||||
|
||||
def _connect_via_proxy(self, host, port):
|
||||
protocol = proxies[proxy_index][0]
|
||||
if protocol == 'http':
|
||||
return self._connect_via_http_proxy(host, port)
|
||||
elif protocol == 'socks5':
|
||||
return self._connect_via_socks5_proxy(host, port)
|
||||
else:
|
||||
raise Exception("不支持的代理类型")
|
||||
|
||||
def _connect_via_http_proxy(self, host, port):
|
||||
proxy_connect = f"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\nConnection: keep-alive\r\n\r\n"
|
||||
remote_socket = socket.create_connection((proxies[proxy_index][1], int(proxies[proxy_index][2])))
|
||||
remote_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 4096)
|
||||
remote_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024 * 4096)
|
||||
remote_socket.sendall(proxy_connect.encode())
|
||||
response = remote_socket.recv(4096).decode('utf-8')
|
||||
if '200 Connection established' not in response:
|
||||
raise Exception(f"无法通过 HTTP 代理建立连接: {response}")
|
||||
return remote_socket
|
||||
|
||||
def _connect_via_socks5_proxy(self, host, port):
|
||||
remote_socket = socks.socksocket()
|
||||
remote_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 4096)
|
||||
remote_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024 * 4096)
|
||||
remote_socket.set_proxy(socks.SOCKS5, proxies[proxy_index][1], int(proxies[proxy_index][2]))
|
||||
remote_socket.connect((host, port))
|
||||
return remote_socket
|
||||
|
||||
def _forward_data(self, client_socket, remote_socket):
|
||||
try:
|
||||
sockets = [client_socket, remote_socket]
|
||||
while True:
|
||||
read_sockets, _, error_sockets = select.select(sockets, [], sockets, 10)
|
||||
if error_sockets:
|
||||
logging.warning("Socket错误,终止连接")
|
||||
break
|
||||
for sock in read_sockets:
|
||||
other_sock = client_socket if sock is remote_socket else remote_socket
|
||||
data = sock.recv(4096)
|
||||
if not data:
|
||||
return
|
||||
other_sock.sendall(data)
|
||||
finally:
|
||||
client_socket.close()
|
||||
remote_socket.close()
|
||||
|
||||
def run(server_class=HTTPServer, handler_class=ProxyHTTPRequestHandler, port=1080, mode='cycle', interval=60, username='neko', password='123456', use_getip=False):
|
||||
global rotate_mode, rotate_interval, proxies, args
|
||||
rotate_mode, rotate_interval = mode, interval
|
||||
|
||||
if use_getip:
|
||||
proxies = get_proxy_from_getip()
|
||||
else:
|
||||
proxies = load_proxies()
|
||||
if proxies:
|
||||
logging.info(f"初始代理地址: {proxies[proxy_index]}")
|
||||
else:
|
||||
logging.error("没有加载到任何代理地址")
|
||||
return
|
||||
|
||||
server_address, max_workers = ('', port), multiprocessing.cpu_count() * 5
|
||||
executor = ThreadPoolExecutor(max_workers=max_workers)
|
||||
server = server_class(server_address, handler_class)
|
||||
server.username = username
|
||||
server.password = password
|
||||
if interval > 0:
|
||||
thread = threading.Thread(target=rotate_proxies, args=(proxies, interval))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
serve_requests(server, executor)
|
||||
|
||||
def serve_requests(server, executor):
|
||||
try:
|
||||
while True:
|
||||
request, client_address = server.get_request()
|
||||
executor.submit(server.process_request, request, client_address)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
executor.shutdown(wait=True)
|
||||
server.server_close()
|
||||
|
||||
def print_icpscan_banner(port, mode, interval, username, password):
|
||||
mode = '循环' if mode == 'cycle' else '单轮'
|
||||
print("--------------------------------------------------------")
|
||||
print("公众号:樱花庄的本间白猫")
|
||||
print("博客:https://y.shironekosan.cn")
|
||||
print("Github:https://github.com/honmashironeko/ProxyCat")
|
||||
print("Gitcode:https://gitcode.com/honmashironeko/ProxyCat")
|
||||
print("--------------------------------------------------------")
|
||||
print(f"监听端口: {port}, 代理轮换模式: {mode}, 代理更换时间: {interval}秒")
|
||||
print(f"默认账号密码: {username}:{password}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=logoprint.logos())
|
||||
parser.add_argument('-p', type=int, default=1080, help='监听端口')
|
||||
parser.add_argument('-m', default='cycle', help='代理轮换模式:cycle 表示循环使用,once 表示用完即止')
|
||||
parser.add_argument('-t', type=int, default=60, help='代理更换时间(秒),设置为0秒时变成每次请求更换IP')
|
||||
parser.add_argument('-up', default='neko:123456', help='指定账号密码,格式为username:password')
|
||||
parser.add_argument('-k', action='store_true', help='使用 getip 模块获取代理地址')
|
||||
args = parser.parse_args()
|
||||
username, password = args.up.split(':')
|
||||
print_icpscan_banner(port=args.p, mode=args.m, interval=args.t, username=username, password=password)
|
||||
run(port=args.p, mode=args.m, interval=args.t, username=username, password=password, use_getip=args.k)
|
||||
@@ -1,30 +1,37 @@
|
||||

|
||||
|
||||
## 一、工具概述
|
||||
[English](./ProxyCat-EN/README-EN.md) | [中文](./README.md)
|
||||
|
||||
渗透过程中总是遇到需要隐藏IP地址或者更换IP地址绕过安全设备的情况,但是市面上的隧道代理价格极其昂贵,普遍在20-40元/天,这高昂的费用令人难以接受。但是笔者注意到短效IP其实性价比很高,一个IP只需要几分钱,平均只需要0.5-3元/天。
|
||||
## 项目概述
|
||||
|
||||
渗透过程中总是遇到需要隐藏IP地址或者更换IP地址绕过安全设备的情况,但是市面上的隧道代理价格极其昂贵,普遍在20-40元/天,这高昂的费用令人难以接受。但是笔者注意到短效IP其实性价比很高,一个IP只需要几分钱,平均只需要0.2-3元/天。
|
||||
|
||||
综上所述,本工具应运而生!目的是在于将持续时间仅有1分钟-60分钟不等的短效IP转变成一个固定IP供其他工具使用,形成代理池服务器,部署一次即可永久使用。
|
||||
|
||||
<img width="1628" alt="Untitled (3)" src="https://github.com/user-attachments/assets/a34b3ac8-1226-4336-b6b5-f6c724537c52">
|
||||

|
||||
|
||||
辛苦各位给本项目,以及作者其他开源项目都点点star,您的star对作者非常重要!也希望您帮忙多多分享本工具。
|
||||
## 支持的功能
|
||||
|
||||
## 二、安装依赖库
|
||||
```
|
||||
本地监听服务类:自定义监听端口、自定义监听端口身份鉴别
|
||||
代理地址类:4种协议支持、代理地址定时更换、代理地址每次请求后更换、API接口自动获取代理地址、支持有身份鉴别的代理地址
|
||||
服务运行时:支持仅在收到请求后才更换代理地址、支持超高并发、支持HTTP、HTTPS请求协议、支持每次启动检查版本更新
|
||||
系统兼容性:Windows、Linux、Mac原则上全支持、支持VPS远程部署、本地部署
|
||||
```
|
||||
|
||||
`pip install -r requirements.txt`
|
||||
## 项目使用
|
||||
|
||||
如果pip安装速度慢,可以采用国内源进行安装:
|
||||
### 安装依赖
|
||||
|
||||
`pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/`
|
||||
工具基于 Python 实现(建议使用python3.8以上版本),在使用前请使用以下命令配置依赖:
|
||||
|
||||
## 三、工具使用
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
# or 推荐使用国内源:
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
|
||||
```
|
||||
|
||||
按照该格式`socks5://127.0.0.1:7890` or` http://127.0.0.1:7890` 一行一个填入ip.txt文件中。
|
||||
|
||||
如果您缺少合适的代理地址,恰巧又有几块钱的话可以联系作者(微信公众号:樱花庄的本间白猫),获取多家低价代理购买地址,为作者带来几毛钱的推荐收益~
|
||||
|
||||
**本工具参数如下所示:**
|
||||
使用 `python` 命令在项目目录运行 `python3 ProxyCat.py -h` 回显如下即配置成功:
|
||||
|
||||
```
|
||||
# python3 ProxyCat.py -h
|
||||
@@ -33,118 +40,175 @@ ZZZzz /,`.-'`' -. ;-;;,_
|
||||
|,4- ) )-,_. ,\ ( `'-'
|
||||
'---''(_/--' `-'\_) ProxyCat
|
||||
|
||||
用法: ProxyCat.py [-h] [-p P] [-m M] [-t T]
|
||||
用法: ProxyCat.py [-h] [-c]
|
||||
|
||||
参数:
|
||||
-h, --help show this help message and exit
|
||||
-p P 监听端口
|
||||
-m M 代理轮换模式:cycle 表示循环使用,once 表示用完即止
|
||||
-t T 代理更换时间(秒),设置为0秒时变成每次请求更换IP
|
||||
-up UP 指定账号密码,格式为username:password
|
||||
-k 使用 getip 模块获取代理地址
|
||||
(默认监听1080端口,默认cycle循环模式,默认60秒更新一次代理,默认账号neko:123456)
|
||||
举例:python3 ProxyCat.py -p 1080 -m cycle -t 180
|
||||
效果:监听1080端口 代理轮换模式为循环使用 每180秒更换到下一个代理地址
|
||||
-h, --help 显示此帮助信息并退出
|
||||
-c C 指定配置文件名(默认config.ini)
|
||||
```
|
||||
|
||||
如果您有现成代理地址获取接口或者付费购买接口,可以使用以下方法
|
||||
### ip.txt 手动录入代理地址
|
||||
|
||||
```
|
||||
# python3 ProxyCat.py -k
|
||||
按照该格式`socks5://127.0.0.1:7890` or` http://127.0.0.1:7890` 一行一个填入ip.txt文件中:
|
||||
|
||||
当您指定-k参数时,不再从本地ip.txt中读取代理地址,而是更换为通过执行getip.py脚本来获取新的代理地址。
|
||||
您可以将getip.py内容修改为您自己的接口,格式为IP:PORT,默认为http协议,如果您要使用socks5,请手动更改。
|
||||
```ip.txt
|
||||
socks5://127.0.0.1:7890
|
||||
socks4://127.0.0.1:7890
|
||||
https://127.0.0.1:7890
|
||||
http://127.0.0.1:7890
|
||||
...
|
||||
```
|
||||
|
||||
**这里以作者推荐的运营商举例**:(请看最下方广告区)
|
||||
> 如果您缺少合适的国内代理地址,恰巧又有几块钱的话可以联系作者(微信公众号:樱花庄的本间白猫),获取多家低价代理购买地址,为作者带来几毛钱的推荐收益~
|
||||
|
||||
<img src="./assets/1724397056696.png" alt="1724397056696" style="zoom: 80%;" />
|
||||
在config.ini(或自定义配置文件)中配置参数:
|
||||
|
||||
当前经过实际测试,在代理地址服务器性能足够的情况下,能够做到100并发不丢包,基本可以覆盖大部分扫描和渗透测试。
|
||||
```
|
||||
[SETTINGS]
|
||||
# 本地服务器监听端口(默认为:1080)
|
||||
port = 1080
|
||||
|
||||
<img src="./assets/f656037af6bd8d6c505aa943f0e972c.png" alt="f656037af6bd8d6c505aa943f0e972c" style="zoom: 50%;" />
|
||||
# 代理地址轮换模式:cycle 表示循环使用,once 表示用完即止(默认为:cycle)
|
||||
mode = cycle
|
||||
|
||||
## 四、工具演示
|
||||
# 代理地址更换时间(秒),设置为 0 时每次请求都更换 IP(默认为:300)
|
||||
interval = 300
|
||||
|
||||
固定代理地址(默认):http://neko:123456@127.0.0.1:1080
|
||||
# 本地服务器端口认证用户名((默认为:neko)当为空时不需要认证
|
||||
username = neko
|
||||
|
||||
# 本地服务器端口认证密码(默认为:123456)当为空时不需要认证
|
||||
password = 123456
|
||||
|
||||
# 是否使用 getip 模块获取代理地址 True or False(默认为:False)
|
||||
use_getip = False
|
||||
|
||||
# 代理地址列表文件(默认为:ip.txt)
|
||||
proxy_file = ip.txt
|
||||
```
|
||||
|
||||
配置对应参数后即可使用:
|
||||
|
||||
```bash
|
||||
# python3 ProxyCat.py
|
||||
```
|
||||
|
||||
**演示效果如下**
|
||||
|
||||
固定代理地址(默认):http://neko:[email protected]:1080 or http://127.0.0.1:1080
|
||||
|
||||
如果您是部署在公网,将127.0.0.1替换成您的公网IP即可。
|
||||
|
||||
<img src="./assets/1724397633981.png" alt="1724397633981" />
|
||||

|
||||
|
||||
### 使用接口自动获取代理地址
|
||||
|
||||
工具支持直接调用代理地址获取的API接口,当您配置`use_getip = True`时,工具将不再从本地ip.txt中读取代理地址,而是更换为通过执行 **getip.py** 脚本来获取新的代理地址。
|
||||
|
||||
## 五、免责申明
|
||||
此时,您需要将 **getip.py** 内容修改为您自己的接口,格式为 `IP:PORT`,默认为 `socks5` 协议,如果您要使用 `http` ,请手动更改。
|
||||
|
||||
1. 如果您下载、安装、使用、修改本工具及相关代码,即表明您信任本工具
|
||||
2. 在使用本工具时造成对您自己或他人任何形式的损失和伤害,我们不承担任何责任
|
||||
3. 如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任
|
||||
4. 请您务必审慎阅读、充分理解各条款内容,特别是免除或者限制责任的条款,并选择接受或不接受
|
||||
5. 除非您已阅读并接受本协议所有条款,否则您无权下载、安装或使用本工具
|
||||
6. 您的下载、安装、使用等行为即视为您已阅读并同意上述协议的约束
|
||||
**演示效果如下**
|
||||
|
||||
## 六、感谢名单
|
||||
> 此处的运营商可在下方广告区域获取
|
||||
|
||||
本排名不分先后,仅用于感谢为本项目提供帮助的师傅们
|
||||

|
||||
|
||||
AabyssZG
|
||||
## 性能表现
|
||||
|
||||
ProbiusOfficial
|
||||
当前经过实际测试,在代理地址服务器性能足够的情况下,能够做到500并发不丢包,基本可以覆盖大部分扫描和渗透测试。
|
||||
|
||||
## 七、开发计划
|
||||

|
||||
|
||||
- [x] 增加代理服务器身份鉴别功能,保证在公网使用过程中不被恶意盗用
|
||||
## 免责申明
|
||||
|
||||
- 如果您下载、安装、使用、修改本工具及相关代码,即表明您信任本工具
|
||||
- 在使用本工具时造成对您自己或他人任何形式的损失和伤害,我们不承担任何责任
|
||||
- 如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任
|
||||
- 请您务必审慎阅读、充分理解各条款内容,特别是免除或者限制责任的条款,并选择接受或不接受
|
||||
- 除非您已阅读并接受本协议所有条款,否则您无权下载、安装或使用本工具
|
||||
- 您的下载、安装、使用等行为即视为您已阅读并同意上述协议的约束
|
||||
|
||||
## 更新日志
|
||||
|
||||
**2024/08/31**
|
||||
|
||||
- 项目大结构调整
|
||||
- 美化显示,将持续提示下一次更换代理地址的时间
|
||||
- 已支持`ctrl+c`停止运行
|
||||
- 大幅度调整为异步请求,并发效率提升,实测1000并发,共5000包,丢50包左右约99%稳定性,500并发0丢包
|
||||
- 不再采取运行时指定参数方案,修改为从本地ini配置文件中读取,易用性更高
|
||||
- 支持本地无认证,适配更多软件代理方式
|
||||
- 增加版本检测功能,将自动提示版本信息
|
||||
- 增加代理服务器地址的身份鉴别功能,但仅支持本地读取,因大多数API都需要白名单,因此不重复提供
|
||||
- 增加功能,仅在收到新请求的情况下才使用getip更新,减少IP消耗
|
||||
- 增加自识别代理服务器地址协议,以适配更多代理商
|
||||
- 增加支持HTTPS、socks4代理协议,目前已覆盖HTTP、HTTPS、socks5、socks4协议
|
||||
|
||||
**2024/08/25**
|
||||
|
||||
- 读取ip.txt时自动跳过空行
|
||||
- httpx更换为并发池,提高性能
|
||||
- 增加缓冲字典,相同站点降低延迟
|
||||
- 每次请求更换IP逻辑修改为,随机选择代理
|
||||
- 采用更加高效的结构和算法,优化请求处理逻辑
|
||||
|
||||
**2024/08/24**
|
||||
|
||||
- 采用异步方案提高并发能力和减少超时
|
||||
- 重复代码封装提高代码复用性
|
||||
|
||||
**2024/08/23**
|
||||
|
||||
- 修改并发逻辑
|
||||
- 增加身份鉴别功能
|
||||
- 增加IP获取接口,永久更换IP
|
||||
- 增加每次请求更换IP功能
|
||||
|
||||
## 开发计划
|
||||
|
||||
- [x] 增加本地服务器身份鉴别功能,保证在公网使用过程中不被恶意盗用
|
||||
|
||||
- [x] 增加每次请求更换IP功能
|
||||
|
||||
- [x] 增加静态代理自动获取更新模块,从而永久运行
|
||||
|
||||
- [ ] 增加负载均衡模式,同时使用大量代理地址发送,从而有效提高并发效率
|
||||
- [ ] 增加负载均衡模式,同时使用大量代理地址发送,从而有效提高并发效率,减少单一服务器负载
|
||||
|
||||
- [ ] 增加版本检测功能
|
||||
- [x] 增加版本检测功能
|
||||
|
||||
- [x] 增加代理地址身份鉴别支持
|
||||
|
||||
- [x] 增加功能,仅在收到新请求的情况下才使用getip更新,减少IP消耗
|
||||
|
||||
- [ ] 首次启动时批量对ip.txt中的代理服务器进行有效性检查
|
||||
|
||||
如果您有好的idea,或者使用过程中遇到的bug,都请辛苦您添加作者联系方式进行反馈!
|
||||
|
||||
微信公众号:樱花庄的本间白猫
|
||||
|
||||
## 八、更新日志
|
||||
## 鸣谢
|
||||
|
||||
**2024/08/25**
|
||||
本排名不分先后,感谢为本项目提供帮助的师傅们。
|
||||
|
||||
1. 读取ip.txt时自动跳过空行
|
||||
2. httpx更换为并发池,提高性能
|
||||
3. 增加缓冲字典,相同站点降低延迟
|
||||
4. 每次请求更换IP逻辑修改为,随机选择代理
|
||||
5. 采用更加高效的结构和算法,优化请求处理逻辑
|
||||
[AabyssZG (曾哥)](https://github.com/AabyssZG)
|
||||
|
||||
**2024/08/24**
|
||||
|
||||
1. 采用异步方案提高并发能力和减少超时
|
||||
2. 重复代码封装提高代码复用性
|
||||
|
||||
**2024/08/23**
|
||||
|
||||
1. 修改并发逻辑
|
||||
2. 增加身份鉴别功能
|
||||
3. 增加IP获取接口,永久更换IP
|
||||
4. 增加每次请求更换IP功能
|
||||
|
||||
**2024/08/21**
|
||||
|
||||
1. 工具首次发布
|
||||
|
||||
## 九、赞助开源
|
||||
|
||||
开源是免费的,全靠爱的力量!但是还是希望您在觉得工具不错的情况下,赞助一下作者的开发哦~
|
||||
|
||||

|
||||
[ProbiusOfficial (探姬)](https://github.com/ProbiusOfficial)
|
||||
|
||||
[](https://star-history.com/#honmashironeko/ProxyCat&Date)
|
||||
|
||||
## 十、代理购买推荐
|
||||
## 赞助开源
|
||||
|
||||
- [第一家代理池购买点击这里跳转](https://www.juliangip.com/user/reg?inviteCode=1033745)
|
||||
- [第二家代理池购买点击这里跳转](https://www.kuaidaili.com/?ref=pglfmm887dmw)
|
||||
<<<<<<< HEAD
|
||||
开源不易,如果您觉得工具不错,或许可以试着赞助一下作者的开发哦~
|
||||
|
||||

|
||||
|
||||
## 代理推荐
|
||||
|
||||
- [第一家便宜大碗代理购买,用邀请码注册得5000免费IP+10元优惠券](https://h.shanchendaili.com/invite_reg.html?invite=fM6fVG)
|
||||
=======
|
||||
- 这几家先停止,存在一定问题,等修复协调好
|
||||
>>>>>>> 54448867afb461da8b865a8b9328dce98853b467
|
||||
- [各大运营商流量卡](https://172.lot-ml.com/ProductEn/Index/0b7c9adef5e9648f)
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 418 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 233 KiB |
@@ -1,3 +0,0 @@
|
||||
http://127.0.0.1:7890
|
||||
socks5://114.132.125.88:7777
|
||||
socks5://47.101.196.233:20002
|
||||
@@ -1,2 +1,3 @@
|
||||
requests
|
||||
pysocks
|
||||
pysocks
|
||||
httpx
|
||||