This commit is contained in:
本间白猫 committed 2024-08-31 16:29:47 +08:00
1 parent a2d4dc154a
commit 2f87d42e67
42 files changed
+1432 -379

No files matched your search

View File
File renamed without changes.
+373
View File
@@ -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("程序被用户中断")
+23
View File
@@ -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
+1 -1
View File
@@ -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]
+1
View File
@@ -0,0 +1 @@
socks5://127.0.0.1:7890
File renamed without changes.
+4
View File
@@ -0,0 +1,4 @@
requests
pysocks
colorama
httpx
+201
View File
@@ -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.
+373
View File
@@ -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")
+211
View File
@@ -0,0 +1,211 @@
![ProxyCat](https://socialify.git.ci/honmashironeko/ProxyCat/image?description=1&descriptionEditable=%E4%B8%80%E6%AC%BE%E8%BD%BB%E9%87%8F%E7%BA%A7%E7%9A%84%E4%BC%98%E7%A7%80%E4%BB%A3%E7%90%86%E6%B1%A0%E4%B8%AD%E9%97%B4%E4%BB%B6%EF%BC%8C%E5%AE%9E%E7%8E%B0%E4%BB%A3%E7%90%86%E7%9A%84%E8%87%AA%E5%8A%A8%E8%BD%AE%E6%8D%A2&font=Bitter&forks=1&issues=1&language=1&logo=https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F139044047%3Fv%3D4&name=1&owner=1&pattern=Circuit%20Board&pulls=1&stargazers=1&theme=Dark)
​ [English](./ProxyCat-EN/README-EN.md) | [中文](./README.md)
## 项目概述
渗透过程中总是遇到需要隐藏IP地址或者更换IP地址绕过安全设备的情况,但是市面上的隧道代理价格极其昂贵,普遍在20-40元/天,这高昂的费用令人难以接受。但是笔者注意到短效IP其实性价比很高,一个IP只需要几分钱,平均只需要0.2-3元/天。
综上所述,本工具应运而生!目的是在于将持续时间仅有1分钟-60分钟不等的短效IP转变成一个固定IP供其他工具使用,形成代理池服务器,部署一次即可永久使用。
![项目原理图](./assets/202408260021207.png)
## 支持的功能
```
本地监听服务类:自定义监听端口、自定义监听端口身份鉴别
代理地址类: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即可。
![界面展示图](./assets/Clip_2024-08-29_10-15-56.png)
### 使用接口自动获取代理地址
工具支持直接调用代理地址获取的API接口,当您配置`use_getip = True`时,工具将不再从本地ip.txt中读取代理地址,而是更换为通过执行 **getip.py** 脚本来获取新的代理地址。
此时,您需要将 **getip.py** 内容修改为您自己的接口,格式为 `IP:PORT`,默认为 `socks5` 协议,如果您要使用 `http` ,请手动更改。
**演示效果如下**
> 此处的运营商可在下方广告区域获取
![1724897980633](./assets/1724897980633.png)
## 性能表现
当前经过实际测试,在代理地址服务器性能足够的情况下,能够做到500并发不丢包,基本可以覆盖大部分扫描和渗透测试。
![8e3f79309626ed0e653ba51b6482bff](./assets/8e3f79309626ed0e653ba51b6482bff.png)
## 免责申明
- 如果您下载、安装、使用、修改本工具及相关代码,即表明您信任本工具
- 在使用本工具时造成对您自己或他人任何形式的损失和伤害,我们不承担任何责任
- 如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任
- 请您务必审慎阅读、充分理解各条款内容,特别是免除或者限制责任的条款,并选择接受或不接受
- 除非您已阅读并接受本协议所有条款,否则您无权下载、安装或使用本工具
- 您的下载、安装、使用等行为即视为您已阅读并同意上述协议的约束
## 更新日志
**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)
[![Star History Chart](https://api.star-history.com/svg?repos=honmashironeko/ProxyCat&type=Date)](https://star-history.com/#honmashironeko/ProxyCat&Date)
## 赞助开源
开源不易,如果您觉得工具不错,或许可以试着赞助一下作者的开发哦~
![ced79e6745867bd51b1eddfb17cb5702](./assets/202408260020820.png)
## 代理推荐
- [第一家便宜大碗代理购买,用邀请码注册得5000免费IP+10元优惠券](https://h.shanchendaili.com/invite_reg.html?invite=fM6fVG)
- [各大运营商流量卡](https://172.lot-ml.com/ProductEn/Index/0b7c9adef5e9648f)
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

+22
View File
@@ -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
+12
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
socks5://127.0.0.1:7890
+58
View File
@@ -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)
+4
View File
@@ -0,0 +1,4 @@
requests
pysocks
colorama
httpx
-292
View File
@@ -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)
+146 -82
View File
@@ -1,30 +1,37 @@
![ProxyCat](https://socialify.git.ci/honmashironeko/ProxyCat/image?description=1&descriptionEditable=%E4%B8%80%E6%AC%BE%E8%BD%BB%E9%87%8F%E7%BA%A7%E7%9A%84%E4%BC%98%E7%A7%80%E4%BB%A3%E7%90%86%E6%B1%A0%E4%B8%AD%E9%97%B4%E4%BB%B6%EF%BC%8C%E5%AE%9E%E7%8E%B0%E4%BB%A3%E7%90%86%E7%9A%84%E8%87%AA%E5%8A%A8%E8%BD%AE%E6%8D%A2&font=Bitter&forks=1&issues=1&language=1&logo=https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F139044047%3Fv%3D4&name=1&owner=1&pattern=Circuit%20Board&pulls=1&stargazers=1&theme=Dark)
## 一、工具概述
​ [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">
![项目原理图](./assets/202408260021207.png)
辛苦各位给本项目,以及作者其他开源项目都点点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" />
![界面展示图](./assets/Clip_2024-08-29_10-15-56.png)
### 使用接口自动获取代理地址
工具支持直接调用代理地址获取的API接口,当您配置`use_getip = True`时,工具将不再从本地ip.txt中读取代理地址,而是更换为通过执行 **getip.py** 脚本来获取新的代理地址。
## 五、免责申明
此时,您需要将 **getip.py** 内容修改为您自己的接口,格式为 `IP:PORT`,默认为 `socks5` 协议,如果您要使用 `http` ,请手动更改。
1. 如果您下载、安装、使用、修改本工具及相关代码,即表明您信任本工具
2. 在使用本工具时造成对您自己或他人任何形式的损失和伤害,我们不承担任何责任
3. 如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任
4. 请您务必审慎阅读、充分理解各条款内容,特别是免除或者限制责任的条款,并选择接受或不接受
5. 除非您已阅读并接受本协议所有条款,否则您无权下载、安装或使用本工具
6. 您的下载、安装、使用等行为即视为您已阅读并同意上述协议的约束
**演示效果如下**
## 六、感谢名单
> 此处的运营商可在下方广告区域获取
本排名不分先后,仅用于感谢为本项目提供帮助的师傅们
![1724897980633](./assets/1724897980633.png)
AabyssZG
## 性能表现
ProbiusOfficial
当前经过实际测试,在代理地址服务器性能足够的情况下,能够做到500并发不丢包,基本可以覆盖大部分扫描和渗透测试。
## 七、开发计划
![8e3f79309626ed0e653ba51b6482bff](./assets/8e3f79309626ed0e653ba51b6482bff.png)
- [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. 工具首次发布
## 九、赞助开源
开源是免费的,全靠爱的力量!但是还是希望您在觉得工具不错的情况下,赞助一下作者的开发哦~
![1724244252220](./assets/1724244252220.png)
[ProbiusOfficial (探姬)](https://github.com/ProbiusOfficial)
[![Star History Chart](https://api.star-history.com/svg?repos=honmashironeko/ProxyCat&type=Date)](https://star-history.com/#honmashironeko/ProxyCat&Date)
## 十、代理购买推荐
## 赞助开源
- [第一家代理池购买点击这里跳转](https://www.juliangip.com/user/reg?inviteCode=1033745)
- [第二家代理池购买点击这里跳转](https://www.kuaidaili.com/?ref=pglfmm887dmw)
<<<<<<< HEAD
开源不易,如果您觉得工具不错,或许可以试着赞助一下作者的开发哦~
![ced79e6745867bd51b1eddfb17cb5702](./assets/202408260020820.png)
## 代理推荐
- [第一家便宜大碗代理购买,用邀请码注册得5000免费IP+10元优惠券](https://h.shanchendaili.com/invite_reg.html?invite=fM6fVG)
=======
- 这几家先停止,存在一定问题,等修复协调好
>>>>>>> 54448867afb461da8b865a8b9328dce98853b467
- [各大运营商流量卡](https://172.lot-ml.com/ProductEn/Index/0b7c9adef5e9648f)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

-3
View File
@@ -1,3 +0,0 @@
http://127.0.0.1:7890
socks5://114.132.125.88:7777
socks5://47.101.196.233:20002
+2 -1
View File
@@ -1,2 +1,3 @@
requests
pysocks
pysocks
httpx