diff --git a/LICENSE b/ProxyCat-CN/LICENSE similarity index 100% rename from LICENSE rename to ProxyCat-CN/LICENSE diff --git a/ProxyCat-CN/ProxyCat-V1.4.py b/ProxyCat-CN/ProxyCat-V1.4.py new file mode 100644 index 0000000..a6ed23b --- /dev/null +++ b/ProxyCat-CN/ProxyCat-V1.4.py @@ -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.+?):(?P.+?)@)?(?P.+)', 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'

(ProxyCat-V\d+\.\d+)

', 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("程序被用户中断") diff --git a/ProxyCat-CN/config.ini b/ProxyCat-CN/config.ini new file mode 100644 index 0000000..1221d4b --- /dev/null +++ b/ProxyCat-CN/config.ini @@ -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 + + diff --git a/getip.py b/ProxyCat-CN/getip.py similarity index 94% rename from getip.py rename to ProxyCat-CN/getip.py index 4236a5c..ff42e86 100644 --- a/getip.py +++ b/ProxyCat-CN/getip.py @@ -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] diff --git a/ProxyCat-CN/ip.txt b/ProxyCat-CN/ip.txt new file mode 100644 index 0000000..c660cad --- /dev/null +++ b/ProxyCat-CN/ip.txt @@ -0,0 +1 @@ +socks5://127.0.0.1:7890 \ No newline at end of file diff --git a/logoprint.py b/ProxyCat-CN/logoprint.py similarity index 100% rename from logoprint.py rename to ProxyCat-CN/logoprint.py diff --git a/ProxyCat-CN/requirements.txt b/ProxyCat-CN/requirements.txt new file mode 100644 index 0000000..98ec39b --- /dev/null +++ b/ProxyCat-CN/requirements.txt @@ -0,0 +1,4 @@ +requests +pysocks +colorama +httpx \ No newline at end of file diff --git a/ProxyCat-EN/LICENSE b/ProxyCat-EN/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/ProxyCat-EN/LICENSE @@ -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. diff --git a/ProxyCat-EN/ProxyCat-V1.4.py b/ProxyCat-EN/ProxyCat-V1.4.py new file mode 100644 index 0000000..05f496b --- /dev/null +++ b/ProxyCat-EN/ProxyCat-V1.4.py @@ -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.+?):(?P.+?)@)?(?P.+)', 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'

(ProxyCat-V\d+\.\d+)

', 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") diff --git a/ProxyCat-EN/README-EN.md b/ProxyCat-EN/README-EN.md new file mode 100644 index 0000000..c93a1bd --- /dev/null +++ b/ProxyCat-EN/README-EN.md @@ -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:123456@127.0.0.1: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) + + + diff --git a/ProxyCat-EN/assets/1724897980633.png b/ProxyCat-EN/assets/1724897980633.png new file mode 100644 index 0000000..c4f3e4a Binary files /dev/null and b/ProxyCat-EN/assets/1724897980633.png differ diff --git a/ProxyCat-EN/assets/202408260020039.png b/ProxyCat-EN/assets/202408260020039.png new file mode 100644 index 0000000..2aae732 Binary files /dev/null and b/ProxyCat-EN/assets/202408260020039.png differ diff --git a/ProxyCat-EN/assets/202408260020611.png b/ProxyCat-EN/assets/202408260020611.png new file mode 100644 index 0000000..d01c407 Binary files /dev/null and b/ProxyCat-EN/assets/202408260020611.png differ diff --git a/ProxyCat-EN/assets/202408260020820.png b/ProxyCat-EN/assets/202408260020820.png new file mode 100644 index 0000000..3128383 Binary files /dev/null and b/ProxyCat-EN/assets/202408260020820.png differ diff --git a/ProxyCat-EN/assets/202408260021207.png b/ProxyCat-EN/assets/202408260021207.png new file mode 100644 index 0000000..01af731 Binary files /dev/null and b/ProxyCat-EN/assets/202408260021207.png differ diff --git a/ProxyCat-EN/assets/24419410a77c91c65ba6dbad5b9097c.png b/ProxyCat-EN/assets/24419410a77c91c65ba6dbad5b9097c.png new file mode 100644 index 0000000..519a036 Binary files /dev/null and b/ProxyCat-EN/assets/24419410a77c91c65ba6dbad5b9097c.png differ diff --git a/ProxyCat-EN/assets/8e3f79309626ed0e653ba51b6482bff.png b/ProxyCat-EN/assets/8e3f79309626ed0e653ba51b6482bff.png new file mode 100644 index 0000000..7bbe2b1 Binary files /dev/null and b/ProxyCat-EN/assets/8e3f79309626ed0e653ba51b6482bff.png differ diff --git a/ProxyCat-EN/assets/Clip_2024-08-29_10-15-56.png b/ProxyCat-EN/assets/Clip_2024-08-29_10-15-56.png new file mode 100644 index 0000000..530279f Binary files /dev/null and b/ProxyCat-EN/assets/Clip_2024-08-29_10-15-56.png differ diff --git a/ProxyCat-EN/config.ini b/ProxyCat-EN/config.ini new file mode 100644 index 0000000..c1abc94 --- /dev/null +++ b/ProxyCat-EN/config.ini @@ -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 + diff --git a/ProxyCat-EN/getip.py b/ProxyCat-EN/getip.py new file mode 100644 index 0000000..abe04ea --- /dev/null +++ b/ProxyCat-EN/getip.py @@ -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 + + diff --git a/ProxyCat-EN/ip.txt b/ProxyCat-EN/ip.txt new file mode 100644 index 0000000..c660cad --- /dev/null +++ b/ProxyCat-EN/ip.txt @@ -0,0 +1 @@ +socks5://127.0.0.1:7890 \ No newline at end of file diff --git a/ProxyCat-EN/logoprint.py b/ProxyCat-EN/logoprint.py new file mode 100644 index 0000000..c5e80e1 --- /dev/null +++ b/ProxyCat-EN/logoprint.py @@ -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) \ No newline at end of file diff --git a/ProxyCat-EN/requirements.txt b/ProxyCat-EN/requirements.txt new file mode 100644 index 0000000..98ec39b --- /dev/null +++ b/ProxyCat-EN/requirements.txt @@ -0,0 +1,4 @@ +requests +pysocks +colorama +httpx \ No newline at end of file diff --git a/ProxyCat_v1.3.py b/ProxyCat_v1.3.py deleted file mode 100644 index bfd04ce..0000000 --- a/ProxyCat_v1.3.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/README.md b/README.md index 62082a9..c7a8513 100644 --- a/README.md +++ b/README.md @@ -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供其他工具使用,形成代理池服务器,部署一次即可永久使用。 -Untitled (3) +![项目原理图](./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 +... ``` -**这里以作者推荐的运营商举例**:(请看最下方广告区) +> 如果您缺少合适的国内代理地址,恰巧又有几块钱的话可以联系作者(微信公众号:樱花庄的本间白猫),获取多家低价代理购买地址,为作者带来几毛钱的推荐收益~ -1724397056696 +在config.ini(或自定义配置文件)中配置参数: -当前经过实际测试,在代理地址服务器性能足够的情况下,能够做到100并发不丢包,基本可以覆盖大部分扫描和渗透测试。 +``` +[SETTINGS] +# 本地服务器监听端口(默认为:1080) +port = 1080 -f656037af6bd8d6c505aa943f0e972c +# 代理地址轮换模式: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:123456@127.0.0.1:1080 or http://127.0.0.1:1080 如果您是部署在公网,将127.0.0.1替换成您的公网IP即可。 -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) diff --git a/assets/1724244252220.png b/assets/1724244252220.png deleted file mode 100644 index 1487d77..0000000 Binary files a/assets/1724244252220.png and /dev/null differ diff --git a/assets/1724397056696.png b/assets/1724397056696.png deleted file mode 100644 index 07de857..0000000 Binary files a/assets/1724397056696.png and /dev/null differ diff --git a/assets/1724397633981.png b/assets/1724397633981.png deleted file mode 100644 index b8240b4..0000000 Binary files a/assets/1724397633981.png and /dev/null differ diff --git a/assets/1724897980633.png b/assets/1724897980633.png new file mode 100644 index 0000000..c4f3e4a Binary files /dev/null and b/assets/1724897980633.png differ diff --git a/assets/1dece93dd5d352a84fd05e9846084c3.png b/assets/1dece93dd5d352a84fd05e9846084c3.png deleted file mode 100644 index d844640..0000000 Binary files a/assets/1dece93dd5d352a84fd05e9846084c3.png and /dev/null differ diff --git a/assets/202408260020039.png b/assets/202408260020039.png new file mode 100644 index 0000000..2aae732 Binary files /dev/null and b/assets/202408260020039.png differ diff --git a/assets/202408260020611.png b/assets/202408260020611.png new file mode 100644 index 0000000..d01c407 Binary files /dev/null and b/assets/202408260020611.png differ diff --git a/assets/202408260020820.png b/assets/202408260020820.png new file mode 100644 index 0000000..3128383 Binary files /dev/null and b/assets/202408260020820.png differ diff --git a/assets/202408260021207.png b/assets/202408260021207.png new file mode 100644 index 0000000..01af731 Binary files /dev/null and b/assets/202408260021207.png differ diff --git a/assets/24419410a77c91c65ba6dbad5b9097c.png b/assets/24419410a77c91c65ba6dbad5b9097c.png new file mode 100644 index 0000000..519a036 Binary files /dev/null and b/assets/24419410a77c91c65ba6dbad5b9097c.png differ diff --git a/assets/8e3f79309626ed0e653ba51b6482bff.png b/assets/8e3f79309626ed0e653ba51b6482bff.png new file mode 100644 index 0000000..7bbe2b1 Binary files /dev/null and b/assets/8e3f79309626ed0e653ba51b6482bff.png differ diff --git a/assets/Clip_2024-08-21_20-24-48.png b/assets/Clip_2024-08-21_20-24-48.png deleted file mode 100644 index b44a8d1..0000000 Binary files a/assets/Clip_2024-08-21_20-24-48.png and /dev/null differ diff --git a/assets/Clip_2024-08-21_20-25-34.png b/assets/Clip_2024-08-21_20-25-34.png deleted file mode 100644 index 6ce8e80..0000000 Binary files a/assets/Clip_2024-08-21_20-25-34.png and /dev/null differ diff --git a/assets/Clip_2024-08-29_10-15-56.png b/assets/Clip_2024-08-29_10-15-56.png new file mode 100644 index 0000000..530279f Binary files /dev/null and b/assets/Clip_2024-08-29_10-15-56.png differ diff --git a/assets/f656037af6bd8d6c505aa943f0e972c.png b/assets/f656037af6bd8d6c505aa943f0e972c.png deleted file mode 100644 index 7d30bea..0000000 Binary files a/assets/f656037af6bd8d6c505aa943f0e972c.png and /dev/null differ diff --git a/ip.txt b/ip.txt deleted file mode 100644 index 10f278f..0000000 --- a/ip.txt +++ /dev/null @@ -1,3 +0,0 @@ -http://127.0.0.1:7890 -socks5://114.132.125.88:7777 -socks5://47.101.196.233:20002 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 02bd782..4ad698a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests -pysocks \ No newline at end of file +pysocks +httpx