Showing preview only (328K chars total). Download the full file or copy to clipboard to get everything.
Repository: honmashironeko/ProxyCat
Branch: main
Commit: 1689d9fe3274
Files: 33
Total size: 315.1 KB
Directory structure:
gitextract_q50giu6l/
├── .gitattributes
├── Dockerfile
├── LICENSE
├── ProxyCat-Manual/
│ ├── Investigation Manual.md
│ ├── Operation Manual.md
│ └── logs.md
├── ProxyCat.py
├── README-EN.md
├── README.md
├── app.py
├── config/
│ ├── blacklist.txt
│ ├── config.ini
│ ├── getip.py
│ ├── ip.txt
│ └── whitelist.txt
├── docker-compose.yml
├── logs/
│ └── proxycat.log
├── modules/
│ ├── modules.py
│ └── proxyserver.py
├── requirements.txt
└── web/
├── static/
│ └── css/
│ ├── animations.css
│ ├── base.css
│ ├── buttons.css
│ ├── dark-mode.css
│ ├── forms.css
│ ├── logs.css
│ ├── nav-tabs.css
│ ├── progress.css
│ ├── responsive.css
│ ├── search.css
│ ├── service-control.css
│ └── status-card.css
└── templates/
└── index.html
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
*.js linguist-language=python
*.css linguist-language=python
*.html linguist-language=python
================================================
FILE: Dockerfile
================================================
FROM python:3.11
WORKDIR /app
COPY requirements.txt .
RUN pip install --upgrade pip -i https://pypi.mirrors.ustc.edu.cn/simple/ && \
pip install --no-cache-dir -r requirements.txt -i https://pypi.mirrors.ustc.edu.cn/simple/
COPY . .
RUN rm -f config/config.ini
VOLUME ["/app/config"]
CMD ["python", "app.py"]
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
================================================
FILE: ProxyCat-Manual/Investigation Manual.md
================================================
Q:出现不正常代理地址,无法正常使用代理

A:检查 API 地址所提供的代理格式是否正确,是否给自己出口IP加白了。
Q:为什么运行 ProxyCat 之后我的IP仍然没有改变?
A:本项目并非全局代理,需要指向本地监听端口,并且保证代理服务器可用,请逐步排查自己的网络。
Q:为什么端口扫描等情况下无法正常代理?
A:端口扫描等任务并非发起HTTP类型等数据包,本工具旨在解决 Web 方面,并不支持其他协议。
Q:为什么我遇到的问题没有在《报错手册》中查到?
A:因为本手册是逐步完善,遇到了就添加,可以联系作者(提问先发50,如果问题无法通过百度或GPT查到解决方案,全额退还。)
Q:出现报错内容 - ERROR - XXXXXXXXXXX
A:将报错内容复制下来到百度上查询。
================================================
FILE: ProxyCat-Manual/Operation Manual.md
================================================
# ProxyCat 使用手册
## 重要事项
- Python版本最好为Python 3.11
- Releases中为较为稳定的打包版本,不一定是最新
- API 接口所获取的代理地址必须为 IP:PORT 格式且只提供一条地址
## 源码使用及 Docker 部署
### 源码手册
**Windows&Mac**:浏览器访问位于 Github 的源码存储库并下载:[ProxyCat](https://github.com/honmashironeko/ProxyCat)

**Linux**:通过 Git 方法拉取项目到本地
```
git clone https://github.com/honmashironeko/ProxyCat.git
```

安装 Python 依赖(**请尽量保证Python版本为3.8-3.11**)
```
pip install -r requirements.txt
# 或使用国内源:
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
```
进入 config 文件夹内找到 config.ini 配置文件,按照自己拥有的资源选择不同的代理服务器获取方法
1️⃣如果您的代理服务器地址为固定的连接,不需要动态更换,可以使用本地 ip.txt 的方式提供格式如下所示
```
# 支持 http/https/socks5 三种代理服务器地址,支持账号密码校验
socks5://neko:123456@127.0.0.1:7890
https://neko:123456@127.0.0.1:7890
http://neko:123456@127.0.0.1:7890
socks5://127.0.0.1:7890
https://127.0.0.1:7890
http://127.0.0.1:7890
...
```
2️⃣如果您是通过 API 方式获取代理地址,可以在 config.ini 中修改配置(配置后不再读取 ip.txt)
```
# config.ini 配置文件中将以下两条配置进行修改
use_getip = True
getip_url = 获取代理地址的 API 接口
# 请注意,API 接口所获取的代理地址必须为 IP:PORT 格式且只提供一条地址,如果格式不同请到 getip.py 脚本中修改代码,如果您需要指定协议(默认为socks5)可以进入 getip.py 脚本中修改
```
当您配置完成之后就可以运行工具了
```
python ProxyCat.py
python app.py (Web控制管理-推荐方式)
```

### Docker 手册
Windows 可以下载 Docker 官方工具:[Docker Desktop](docs.dockerd.com.cn)

Linux 可以通过清华大学源提供的脚本一键安装:[清华大学安装脚本](https://mirrors.tuna.tsinghua.edu.cn/help/docker-ce/)

安装完成后请测试docker、docker-compose是否安装成功,如果安装失败请百度
Windows&Linux 进入 ProxyCat 文件夹下(**在此之前请根据源码手册中 config.ini 配置部分完成参数修改**)运行以下命令进行部署
```
# 进入ProxyCat文件夹中并构建镜像和启动容器
docker-compose up -d --build
# 停止服务和启动服务(每次修改完配置后需要重启服务)
docker-compose down | docker-compose up -d
# 查看日志信息
docker logs proxycat
# docker端口默认为1080和5000,1080为监听端口,5000为web页面管理如需其他端口请对应修改并放行
```
### 配置文件介绍
```
# 日志显示级别(默认为:1)
# 0: 仅显示代理切换和错误信息
# 1: 显示代理切换、倒计时和错误信息
# 2: 显示所有详细信息
# 仅终端管理时生效
display_level = 1
# 本地服务器监听端口(默认为:1080)
# Local server listening port (default:1080)
port = 1080
# Web 管理页面端口(默认为:5000)
web_port = 5000
# 代理地址轮换模式:cycle 表示循环使用,loadbalance 表示负载均衡(默认为:cycle)
# Proxy rotation mode: cycle means cyclic use, loadbalance means load balancing (default:cycle)
mode = cycle
# 代理地址更换时间(秒),设置为 0 时每次请求都更换 IP(默认为:300)
# Proxy address rotation interval (seconds), when set to 0, IP changes with each request (default:300)
interval = 300
# 是否使用 getip 模块获取代理地址 True or False(默认为:False)
# Whether to use getip module to obtain proxy addresses True or False (default:False)
use_getip = False
# 获取新代理地址的URL
# URL to get new proxy address
getip_url = http://example.com/getip
# 代理服务器认证用户名(如果代理服务器需要认证)
# Proxy server authentication username (if proxy server requires authentication)
proxy_username =
# 代理服务器认证密码(如果代理服务器需要认证)
# Proxy server authentication password (if proxy server requires authentication)
proxy_password =
# 代理地址列表文件(默认为:ip.txt)
# Proxy address list file (default:ip.txt)
proxy_file = ip.txt
# 是否启用代理检测功能 True or False(默认为True)
# Whether to enable proxy detection feature True or False (default:True)
check_proxies = True
# 语言设置 (cn/en)
# Language setting (cn/en)
language = cn
# IP白名单文件路径(留空则不启用白名单)
# IP whitelist file path (leave empty to disable whitelist)
whitelist_file = whitelist.txt
# IP黑名单文件路径(留空则不启用黑名单)
# IP blacklist file path (leave empty to disable blacklist)
blacklist_file = blacklist.txt
# IP认证优先级(whitelist/blacklist)
# IP authentication priority (whitelist/blacklist)
# whitelist: 优先判断白名单,在白名单中的IP直接放行
# whitelist: prioritize whitelist check, IPs in whitelist are allowed directly
# blacklist: 优先判断黑名单,在黑名单中的IP直接拒绝
# blacklist: prioritize blacklist check, IPs in blacklist are rejected directly
ip_auth_priority = whitelist
# Web 管理页面访问token,留空则无需token(默认为:honmashironeko)
token = honmashironeko
# 在[Users]下面是用户管理组,"账号=密码"一行一个,留空时代理无需身份鉴别(默认为:neko=123456)
[Users]
neko=123456
```
### Web 控制面板
采用源码部署的话通过 **python app.py** 启动 Web 控制面板,并根据提示访问 Web




## 问题Q&A
Q:为什么运行后我的XXX工具代理还是没换?
A:ProxyCat 并不是全局代理工具,需要XXX工具支持使用代理,将流量发送到 ProxyCat 的本地监听端口才会经过代理池。
Q:为什么倒计时结束后代理没有更换?
A:ProxyCat 为了节约硬件资源和代理服务器资源,特意修改运行逻辑为有流量经过的时候才会更换代理,这样可以减少资源的浪费,同时可以部署一次,长期可用。
Q:为什么我用 getip 方式获取代理地址的时候,首次运行会报 None ,没有可用的代理地址?
A:为了防止资源浪费,通过 getip 获取的情况一般是付费购买静态短效IP,运行就获取的话会浪费大量资源从而导致资金损耗,为避免这种情况发生,首次运行不会主动获取,您只需要正常使用发包,ProxyCat 会自动获取并发送。
Q:getip.py 当中的appKey和anquanma是做什么的?
A:这两个参数是用作自动将当前请求IP添加到服务商(请查看readme中最下面的第一个推荐)的白名单中,免去每次IP变更需重新添加的烦恼,其中anquanma(安全码)需要到个人中心配置。
Q:我自己有静态IP提供地址该怎么用?
A:将地址填入getip_url,如果有账号密码请写在 proxy_username 、proxy_password 。
Q:为什么我会遇到 XXX 报错?为什么不能用?
A:可先看[《排查手册》](https://github.com/honmashironeko/ProxyCat/blob/main/ProxyCat-Manual/Investigation%20Manual.md),无法修复的情况下可以找作者询问,提问前请先支付50元作为时间的购买费用,如果您的问题属于百度可查或手册中有的,费用将不会返还;如果属于工具BUG或功能建议,费用将全额返还并将您列入本项目的感谢名单中。(实在是太多人多在问一些非常简单且写在帮助中的问题,时间被极大的浪费了,同时有很多态度非常恶劣的人,这不是我所想要的)
================================================
FILE: ProxyCat-Manual/logs.md
================================================
### 2025/03/23
- 修复负载均衡模式无法调用的BUG
- 修复socks5连接错误
- 修复http、socks5监听下的目标网站错误和代理地址失效情况一致导致无法正常触发代理切换
- 修改代理有效性校验,配置为可控检测,关闭后将不会进行有效性检查避免特殊情况一直切换
- 修复并发下导致大规模触发更换和提示的问题,锁定操作的原子性
- 修复大量细节逻辑、描述错误
- 当前代理切换触发条件为:时间间隔到期切换、代理失效自动切换、Web手动切换、API下首次请求自动获取
### 2025/03/17
- 修复目标站点本身错误时会触发代理切换的错误逻辑
- 修改连接关闭方式
- 优化监听服务器性能
- 修复多处错误BUG
### 2025/03/14
- 修复'_last_used'报错问题,连接关闭方式修正
- 修复本地代理读取时切换逻辑失效问题
- 增加切换时间间隔设置为0时进入每次请求都更换IP
### 2025/03/03
- 美化 Web管理界面
- 修复大量 BUG
- 添加更多处理小脚本辅助使用
### 2025/02/21
- 增加 Web 管理界面
- 增加多用户模式
- 代码结构大改
- config.in及相关文件动态更新不需要重启
- 增加日志显示级别控制
- 增加记录连接人信息日志,包括连接的IP和使用的账号密码
- 以及其他乱七八糟的修改,这次大版本更新改的太多,我有点忘记了~
### 2025/02/06
- Docker 安装依赖库采用国内源
- 增加Getip方式下主动提示:当前为API模式,收到请求将自动获取代理地址
- 修改加白方式,自动根据请求结果加白
### 2025/01/14
- 增加 getip 方式下自动添加白名单机制。
- 支持带有账号密码的本地读取、getip获取、有效性校验的代理地址。、
- 整理代码结构,合并一些代码、删除一些多余代码。
### 2025/01/07
- 引入连接池机制提高性能。
- 优化部分错误处理和日志记录。
- 代理切换机制优化。
### 2025/01/03
- 集中配置参数到配置文件中管理,提升维护便利。
- 修复部分已知BUG,并提升稳定性和并发能力。
### 2025/01/02
- 重构软件结构,更加整洁易用。
- 新增支持黑白名单机制进行身份认证。
- 在使用GetIP方式的时候,需要先收到一次请求才会获取代理,防止每次运行都浪费资金。
- 语言配置逻辑更改,不再分为两个版本,通过config.ini文件中的语言配置参数进行显示。
- 配置信息面板更新,不配置账号密码的情况下也能直接复制地址使用。
- 新增docker方式部署。
### **2024/10/23**
- 重构代码结构,将部分代码分割成单独文件。
- 支持代理过程中,遇到代理服务器突然失效,自动请求更换新的代理服务器,并重置更换计时器。
### 2024/09/29
- 去除使用较少的单次循环,更换为自定义模式,可根据需求自定义更换代理的逻辑。
- 对代理有效性检测修改为异步,提高速度。
- 去除问题较多的 SOCKS4 协议的代理支持。
- 对日志系统进行美化。
- 改进异常处理逻辑。
- 增加对代理格式的校验,确保格式正确。
### 2024/09/10
- 优化并发效率,支持在未收到响应包的情况下提前进行下一个请求,提高效率。
- 增加负载均衡模式,该模式下将随机向代理地址发送请求,并利用并发代理的方式,提高请求效率。
- 代理有效性检测修改为异步,提高效率。
### 2024/09/09
- 增加功能,可设置首次启动时是否对 `ip.txt` 中的代理地址进行有效性校验,并只使用有效的代理地址。
- 函数降级,支持更低版本的 Python。
### 2024/09/03
- 增加本地 SOCKS5 监听,适配更多软件。
- 部分函数更换,适配更低版本的 Python。
- 美化回显内容。
### 2024/08/31
- 项目大结构调整。
- 美化显示,持续提示下一次更换代理地址的时间。
- 支持 `Ctrl+C` 停止运行。
- 大幅度调整为异步请求,并发效率提升,实测 **1000** 并发,共 **5000** 包,丢包约 **50** 包,稳定性约 **99%**,**500** 并发无丢包。
- 不再采取运行时指定参数方案,修改为从本地 `ini` 配置文件中读取,易用性更高。
- 支持本地无认证,适配更多软件代理方式。
- 增加版本检测功能,自动提示版本信息。
- 增加代理服务器地址的身份鉴别功能,仅支持本地读取,因大多数 API 需白名单,未提供重复。
- 增加功能,仅在收到新请求的情况下才使用 `getip` 更新,减少 IP 消耗。
- 增加自识别代理服务器地址协议,以适配更多代理商。
- 增加支持 HTTPS、SOCKS4 代理协议,目前已覆盖 HTTP、HTTPS、SOCKS5、SOCKS4 协议。
- 修改 `asyncio.timeout()` 为 `asyncio.wait_for()`,适配更低的 Python 版本。
### 2024/08/25
- 读取 `ip.txt` 时自动跳过空行。
- 将 `httpx` 更换为并发池,提高性能。
- 增加缓冲字典,相同站点降低延迟。
- 每次请求更换 IP 逻辑修改为随机选择代理。
- 采用更高效的结构和算法,优化请求处理逻辑。
### 2024/08/24
- 采用异步方案提高并发能力和减少超时。
- 重复代码封装,提高代码复用性。
### 2024/08/23
- 修改并发逻辑。
- 增加身份鉴别功能。
- 增加 IP 获取接口,永久更换 IP。
- 增加每次请求更换 IP 功能。
================================================
FILE: ProxyCat.py
================================================
from wsgiref import headers
from modules.modules import ColoredFormatter, load_config, DEFAULT_CONFIG, check_proxies, check_for_updates, get_message, load_ip_list, print_banner, logos
import threading, argparse, logging, asyncio, time, socket, signal, sys, os
from concurrent.futures import ThreadPoolExecutor
from modules.proxyserver import AsyncProxyServer
from colorama import init, Fore, Style
from itertools import cycle
from tqdm import tqdm
import base64
from configparser import ConfigParser
init(autoreset=True)
def setup_logging():
log_format = '%(asctime)s - %(levelname)s - %(message)s'
formatter = ColoredFormatter(log_format)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logging.basicConfig(level=logging.INFO, handlers=[console_handler])
def update_status(server):
def print_proxy_info():
status = f"{get_message('current_proxy', server.language)}: {server.current_proxy}"
logging.info(status)
def reload_server_config(new_config):
old_use_getip = server.use_getip
old_mode = server.mode
old_port = int(server.config.get('port', '1080'))
server.config.update(new_config)
server._update_config_values(new_config)
if old_use_getip != server.use_getip or old_mode != server.mode:
server._handle_mode_change()
if old_port != server.port:
logging.info(get_message('port_changed', server.language, old_port, server.port))
config_file = 'config/config.ini'
ip_file = server.proxy_file
last_config_modified_time = os.path.getmtime(config_file) if os.path.exists(config_file) else 0
last_ip_modified_time = os.path.getmtime(ip_file) if os.path.exists(ip_file) else 0
display_level = int(server.config.get('display_level', '1'))
is_docker = os.path.exists('/.dockerenv')
while True:
try:
if os.path.exists(config_file):
current_config_modified_time = os.path.getmtime(config_file)
if current_config_modified_time > last_config_modified_time:
logging.info(get_message('config_file_changed', server.language))
new_config = load_config(config_file)
reload_server_config(new_config)
last_config_modified_time = current_config_modified_time
continue
if os.path.exists(ip_file) and not server.use_getip:
current_ip_modified_time = os.path.getmtime(ip_file)
if current_ip_modified_time > last_ip_modified_time:
logging.info(get_message('proxy_file_changed', server.language))
server._reload_proxies()
last_ip_modified_time = current_ip_modified_time
continue
if display_level == 0:
if not hasattr(server, 'last_proxy') or server.last_proxy != server.current_proxy:
print_proxy_info()
server.last_proxy = server.current_proxy
time.sleep(1)
continue
if server.mode == 'loadbalance':
if display_level >= 1:
print_proxy_info()
time.sleep(5)
continue
time_left = server.time_until_next_switch()
if time_left == float('inf'):
if display_level >= 1:
print_proxy_info()
time.sleep(5)
continue
if not hasattr(server, 'last_proxy') or server.last_proxy != server.current_proxy:
print_proxy_info()
server.last_proxy = server.current_proxy
server.previous_proxy = server.current_proxy
total_time = int(server.interval)
elapsed_time = total_time - int(time_left)
if display_level >= 1:
if elapsed_time > total_time:
if hasattr(server, 'progress_bar'):
if not is_docker:
server.progress_bar.n = total_time
server.progress_bar.refresh()
server.progress_bar.close()
delattr(server, 'progress_bar')
if hasattr(server, 'last_update_time'):
delattr(server, 'last_update_time')
time.sleep(0.5)
continue
if is_docker:
if not hasattr(server, 'last_update_time') or \
(time.time() - server.last_update_time >= (5 if display_level == 1 else 1) and elapsed_time <= total_time):
if display_level >= 2:
logging.info(f"{get_message('next_switch', server.language)}: {time_left:.0f} {get_message('seconds', server.language)} ({elapsed_time}/{total_time})")
else:
logging.info(f"{get_message('next_switch', server.language)}: {time_left:.0f} {get_message('seconds', server.language)}")
server.last_update_time = time.time()
else:
if not hasattr(server, 'progress_bar'):
server.progress_bar = tqdm(
total=total_time,
desc=f"{Fore.YELLOW}{get_message('next_switch', server.language)}{Style.RESET_ALL}",
bar_format='{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} ' + get_message('seconds', server.language),
colour='green'
)
server.progress_bar.n = min(elapsed_time, total_time)
server.progress_bar.refresh()
except Exception as e:
if display_level >= 2:
logging.error(f"Status update error: {e}")
elif display_level >= 1:
logging.error(get_message('status_update_error', server.language))
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(get_message('client_handle_error', server.language, e))
finally:
clients.remove(task)
async def run_server(server):
try:
await server.start()
except asyncio.CancelledError:
logging.info(get_message('server_closing', server.language))
except Exception as e:
if not server.stop_server:
logging.error(f"Server error: {e}")
finally:
await server.stop()
async def run_proxy_check(server):
if server.config.get('check_proxies', 'False').lower() == 'true':
logging.info(get_message('proxy_check_start', server.language))
valid_proxies = await check_proxies(server.proxies, server.test_url)
if valid_proxies:
server.proxies = valid_proxies
server.proxy_cycle = cycle(valid_proxies)
server.current_proxy = next(server.proxy_cycle)
logging.info(get_message('valid_proxies', server.language, valid_proxies))
else:
logging.error(get_message('no_valid_proxies', server.language))
else:
logging.info(get_message('proxy_check_disabled', server.language))
class ProxyCat:
def __init__(self):
cpu_count = os.cpu_count() or 1
self.executor = ThreadPoolExecutor(
max_workers=min(32, cpu_count + 4),
thread_name_prefix="proxy_worker",
thread_name_format="proxy_worker_%d"
)
loop = asyncio.get_event_loop()
loop.set_default_executor(self.executor)
if hasattr(loop, 'set_task_factory'):
loop.set_task_factory(None)
socket.setdefaulttimeout(30)
if hasattr(socket, 'TCP_NODELAY'):
socket.TCP_NODELAY = True
if hasattr(socket, 'SO_KEEPALIVE'):
socket.SO_KEEPALIVE = True
if hasattr(socket, 'SO_REUSEADDR'):
socket.SO_REUSEADDR = True
if hasattr(socket, 'SO_REUSEPORT') and os.name != 'nt':
socket.SO_REUSEPORT = True
if os.name != 'nt':
import resource
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
try:
resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard))
except ValueError:
resource.setrlimit(resource.RLIMIT_NOFILE, (soft, soft))
soft, hard = resource.getrlimit(resource.RLIMIT_NPROC)
try:
resource.setrlimit(resource.RLIMIT_NPROC, (hard, hard))
except ValueError:
pass
self.running = True
self.tasks = set()
self.max_tasks = 20000
self.task_semaphore = asyncio.Semaphore(self.max_tasks)
signal.signal(signal.SIGINT, self.handle_shutdown)
signal.signal(signal.SIGTERM, self.handle_shutdown)
self.config = load_config('config/config.ini')
self.language = self.config.get('language', 'cn').lower()
self.users = {}
config = ConfigParser()
config.read('config/config.ini', encoding='utf-8')
if config.has_section('Users'):
self.users = dict(config.items('Users'))
self.auth_required = bool(self.users)
self.proxy_pool = {}
self.max_connections = 1000
self.connection_timeout = 30
self.read_timeout = 60
async def start_server(self):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if hasattr(socket, 'SO_REUSEPORT') and os.name != 'nt':
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.bind((self.config.get('SERVER', 'host'), int(self.config.get('SERVER', 'port'))))
server = await asyncio.start_server(
self.handle_client,
sock=sock
)
logging.info(get_message('server_running', self.language,
self.config.get('SERVER', 'host'),
self.config.get('SERVER', 'port')))
async with server:
await server.serve_forever()
except Exception as e:
logging.error(get_message('server_start_error', self.language, e))
sys.exit(1)
def handle_shutdown(self, signum, frame):
logging.info(get_message('server_shutting_down', self.language))
self.running = False
self.executor.shutdown(wait=True)
sys.exit(0)
async def handle_client(self, reader, writer):
task = asyncio.current_task()
self.tasks.add(task)
try:
if self.auth_required:
auth_header = headers.get('proxy-authorization')
if not auth_header or not self._authenticate(auth_header):
writer.write(b'HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="Proxy"\r\n\r\n')
await writer.drain()
return
await asyncio.get_event_loop().run_in_executor(
self.executor,
self._handle_proxy_request,
reader,
writer
)
except Exception as e:
logging.error(get_message('client_process_error', self.language, e))
finally:
try:
writer.close()
await writer.wait_closed()
except:
pass
self.tasks.remove(task)
def _authenticate(self, auth_header):
if not self.users:
return True
try:
scheme, credentials = auth_header.split()
if scheme.lower() != 'basic':
return False
decoded_auth = base64.b64decode(credentials).decode()
username, password = decoded_auth.split(':')
return username in self.users and self.users[username] == password
except:
return False
async def _handle_proxy_request(self, reader, writer):
try:
request_line = await reader.readline()
if not request_line:
return
method, target, version = request_line.decode().strip().split(' ')
if method == 'CONNECT':
await self._handle_connect(target, reader, writer)
else:
await self._handle_http(method, target, version, reader, writer)
except Exception as e:
logging.error(get_message('request_handling_error', self.language, e))
try:
writer.close()
except:
pass
async def _handle_connect(self, target, reader, writer):
host, port = target.split(':')
port = int(port)
try:
remote_reader, remote_writer = await asyncio.wait_for(
asyncio.open_connection(host, port),
timeout=self.connection_timeout
)
writer.write(b'HTTP/1.1 200 Connection Established\r\n\r\n')
await writer.drain()
await self._create_pipe(reader, writer, remote_reader, remote_writer)
except Exception as e:
logging.error(get_message('proxy_forward_error', self.language, e))
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
async def _handle_http(self, method, target, version, reader, writer):
try:
from urllib.parse import urlparse
url = urlparse(target)
host = url.hostname
port = url.port or 80
remote_reader, remote_writer = await asyncio.wait_for(
asyncio.open_connection(host, port),
timeout=self.connection_timeout
)
path = url.path + ('?' + url.query if url.query else '')
request = f'{method} {path} {version}\r\n'
remote_writer.write(request.encode())
while True:
line = await reader.readline()
if line == b'\r\n':
break
remote_writer.write(line)
remote_writer.write(b'\r\n')
await self._create_pipe(reader, writer, remote_reader, remote_writer)
except Exception as e:
logging.error(get_message('proxy_forward_error', self.language, e))
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
async def _create_pipe(self, client_reader, client_writer, remote_reader, remote_writer):
try:
pipe1 = asyncio.create_task(self._pipe(client_reader, remote_writer))
pipe2 = asyncio.create_task(self._pipe(remote_reader, client_writer))
done, pending = await asyncio.wait(
[pipe1, pipe2],
return_when=asyncio.FIRST_COMPLETED
)
for task in pending:
task.cancel()
except Exception as e:
logging.error(get_message('data_transfer_error', self.language, e))
finally:
try:
remote_writer.close()
await remote_writer.wait_closed()
except:
pass
async def _pipe(self, reader, writer):
try:
while True:
data = await reader.read(8192)
if not data:
break
writer.write(data)
await writer.drain()
except asyncio.CancelledError:
pass
except Exception as e:
logging.error(get_message('data_transfer_error', self.language, e))
def monitor_resources(self):
import psutil
process = psutil.Process(os.getpid())
while self.running:
mem_info = process.memory_info()
logging.debug(f"Memory usage: {mem_info.rss / 1024 / 1024:.2f} MB, "
f"Connections: {len(self.tasks)}")
time.sleep(60)
if __name__ == '__main__':
setup_logging()
parser = argparse.ArgumentParser(description=logos())
parser.add_argument('-c', '--config', default='config/config.ini', help='配置文件路径')
args = parser.parse_args()
config = load_config(args.config)
server = AsyncProxyServer(config)
print_banner(config)
asyncio.run(check_for_updates(config.get('language', 'cn').lower()))
if not config.get('use_getip', 'False').lower() == 'true':
asyncio.run(run_proxy_check(server))
else:
logging.info(get_message('api_mode_notice', server.language))
status_thread = threading.Thread(target=update_status, args=(server,), daemon=True)
status_thread.start()
cleanup_thread = threading.Thread(target=lambda: asyncio.run(server.cleanup_clients()), daemon=True)
cleanup_thread.start()
try:
asyncio.run(run_server(server))
except KeyboardInterrupt:
logging.info(get_message('user_interrupt', server.language))
================================================
FILE: README-EN.md
================================================

<p align="center">
<a href="/README-EN.md">English</a>
·
<a href="/README.md">简体中文</a>
</p>
## Table of Contents
- [Development Background](#development-background)
- [Features](#features)
- [Installation and Usage](#installation-and-usage)
- [Disclaimer](#disclaimer)
- [Changelog](#changelog)
- [Development Plan](#development-plan)
- [Special Thanks](#special-thanks)
- [Sponsor](#sponsor)
- [Proxy Recommendations](#proxy-recommendations)
## Development Background
During penetration testing, it's often necessary to hide or change IP addresses to bypass security devices. However, tunnel proxies in the market are expensive, typically costing $3-6 per day, which is unaffordable for many. The author noticed that short-term IPs offer high cost-effectiveness, with each IP costing just a few cents, averaging $0.03-0.4 per day.
Therefore, **ProxyCat** was born! This tool aims to transform short-term IPs (lasting from 1 to 60 minutes) into fixed IPs for other tools to use, creating a proxy pool server that can be used permanently after one deployment.

## Features
- **Dual Protocol Listening**: Supports HTTP/SOCKS5 protocol listening, compatible with more tools.
- **Triple Proxy Types**: Supports HTTP/HTTPS/SOCKS5 proxy servers with authentication.
- **Flexible Switching Modes**: Supports sequential, random, and custom proxy selection for optimized traffic distribution.
- **Dynamic Proxy Acquisition**: Get available proxies in real-time through GetIP function, supports API interface calls.
- **Proxy Protection**: When using GetIP method, proxies are only fetched upon receiving requests, not at initial startup.
- **Automatic Proxy Detection**: Automatically checks proxy validity at startup, removing invalid ones.
- **Smart Proxy Switching**: Only obtains new proxies during request execution, reducing resource consumption.
- **Invalid Proxy Handling**: Automatically validates and switches to new proxies when current ones fail.
- **Authentication Support**: Supports username/password authentication and IP blacklist/whitelist management.
- **Real-time Status Display**: Shows proxy status and switching times for dynamic monitoring.
- **Dynamic Configuration**: Updates configuration without service restart.
- **Web UI Interface**: Provides web management interface for convenient operation.
- **Docker Deployment**: One-click Docker deployment with unified web management.
- **Bilingual Support**: Supports Chinese and English language switching.
- **Flexible Configuration**: Customize ports, modes, and authentication through config.ini.
- **Version Check**: Automatic software update checking.
## Tool Usage
[ProxyCat Operation Manual](../main/ProxyCat-Manual/Operation%20Manual.md)
## Error Troubleshooting
[ProxyCat Investigation Manual](../main/ProxyCat-Manual/Investigation%20Manual.md)
## Disclaimer
- By downloading, installing, using, or modifying this tool and related code, you indicate your trust in this tool.
- We are not responsible for any form of loss or damage caused to yourself or others while using this tool.
- You are solely responsible for any illegal activities conducted while using this tool.
- Please carefully read and fully understand all terms, especially liability exemption clauses.
- You have no right to download, install, or use this tool unless you have read and accepted all terms.
- Your download, installation, and usage actions indicate your acceptance of this agreement.
## Changelog
[Changelog Records](../main/ProxyCat-Manual/logs.md)
## Development Plan
- [x] Add detailed logging to record all IP identities connecting to ProxyCat, supporting multiple users.
- [x] Add Web UI for a more powerful and user-friendly interface.
- [ ] Develop babycat module that can run on any server or host to turn it into a proxy server.
- [ ] Add request blacklist/whitelist to specify URLs, IPs, or domains to be forcibly dropped or bypassed.
- [ ] Package to PyPi for easier installation and use.
If you have good ideas or encounter bugs during use, please contact the author through:
WeChat Official Account: **樱花庄的本间白猫**
## Special Thanks
In no particular order, thanks to all contributors who helped with this project:
- [AabyssZG (曾哥)](https://github.com/AabyssZG)
- [ProbiusOfficial (探姬)](https://github.com/ProbiusOfficial)
- [gh0stkey (EvilChen)](https://github.com/gh0stkey)
- [huangzheng2016(HydrogenE7)](https://github.com/huangzheng2016)
- chars6
- qianzai(千载)
- ziwindlu
## Sponsor
Open source development isn't easy. If you find this tool helpful, consider sponsoring the author's development!
---
| Rank | ID | Amount (CNY) |
| :--: | :-----------------: | :----------: |
| 1 | **陆沉** | 1266.62 |
| 2 | **柯林斯.民间新秀** | 696 |
| 3 | **北** | 170 |
| [Sponsor List](https://github.com/honmashironeko/Thanks-for-sponsorship) | Every sponsorship is a motivation for the author! | (´∀`)♡ |
---

## Proxy Recommendations
- [First affordable proxy service - Get 5000 free IPs + ¥10 coupon with invite code](https://h.shanchendaili.com/invite_reg.html?invite=fM6fVG)
- [Various carrier data plans](https://172.lot-ml.com/ProductEn/Index/0b7c9adef5e9648f)
- [Click here to purchase](https://www.ipmart.io?source=Shironeko)
================================================
FILE: README.md
================================================

<p align="center">
<a href="/README-EN.md">English</a>
·
<a href="/README.md">简体中文</a>
</p>
## 目录
- [开发缘由](#开发缘由)
- [功能特点](#功能特点)
- [安装与使用](#安装与使用)
- [免责申明](#免责申明)
- [更新日志](#更新日志)
- [开发计划](#开发计划)
- [特别鸣谢](#特别鸣谢)
- [赞助开源](#赞助开源)
- [代理推荐](#代理推荐)
## 开发缘由
在渗透过程中,经常需要隐藏或更换IP地址以绕过安全设备。然而,市面上的隧道代理价格高昂,普遍在20-40元/天,这对于许多人来说难以接受。笔者注意到,短效IP的性价比很高,一个IP只需几分钱,平均每天0.2-3元。
综上所述,**ProxyCat** 应运而生!本工具旨在将持续时间仅有1分钟至60分钟不等的短效IP转变为固定IP供其他工具使用,形成代理池服务器,部署一次即可永久使用。

## 功能特点
- **两种协议监听**:支持 HTTP/SOCKS5 协议监听,兼容更多工具。
- **三种代理地址**:支持 HTTP/HTTPS/SOCKS5 代理服务器及身份鉴别。
- **灵活切换模式**:支持顺序、随机及自定义代理选择,优化流量分配。
- **动态获取代理**:通过 GetIP 函数即时获取可用代理,支持 API 接口调用。
- **代理保护机制**:在使用 GetIP 方式获取代理时,首次运行不会直接请求获取,将会在收到请求的时候才获取。
- **自动代理检测**:启动时自动检测代理有效性,剔除无效代理。
- **智能切换代理**:仅在请求运行时获取新代理,减少资源消耗。
- **失效代理切换**:代理失效后自动验证切换新代理,确保不中断服务。
- **身份认证支持**:支持用户名/密码认证和黑白名单管理,提高安全性。
- **实时状态显示**:展示代理状态和切换时间,实时掌握代理动态。
- **动态更新配置**:无需重启服务,动态检测配置并更新。
- **Web UI界面**:提供 Web 管理界面,操作管理更加便捷。
- **Docker部署**:Docker 一键部署,Web 统一管理。
- **中英文双语**:支持中文英文一键切换。
- **配置灵活**:通过 config.ini 文件自定义端口、模式和认证信息等。
- **版本检测**:自动检查软件更新,保证版本最新。
## 工具使用
[ProxyCat操作手册](../main/ProxyCat-Manual/Operation%20Manual.md)
## 报错排查
[ProxyCat排查手册](../main/ProxyCat-Manual/Investigation%20Manual.md)
## 免责申明
- 如果您下载、安装、使用、修改本工具及相关代码,即表明您信任本工具。
- 在使用本工具时造成对您自己或他人任何形式的损失和伤害,我们不承担任何责任。
- 如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任。
- 请您务必审慎阅读、充分理解各条款内容,特别是免除或者限制责任的条款,并选择接受或不接受。
- 除非您已阅读并接受本协议所有条款,否则您无权下载、安装或使用本工具。
- 您的下载、安装、使用等行为即视为您已阅读并同意上述协议的约束。
## 更新日志
[更新日志记录](../main/ProxyCat-Manual/logs.md)
## 开发计划
- [ ] **增加请求切换IP配置**:支持设置随机范围内的请求次数来切换IP,提升对抗阈值类防御的能力。
- [ ] **babycat模块**:通过部署babycat子模块,可以在任意服务器或主机上快速搭建代理服务器,并通过Web端实现统一管理,简化操作流程。(针对红队进行开发强化)
- [ ] **爬虫代理池**:利用爬虫抓取免费代理地址,构建代理池。该代理池能够持续维护可用的高质量代理资源,支持负载均衡下的随机调用,同时允许用户指定IP归属地,满足不同场景需求。
- [ ] **机场协议支持**:接入机场协议后,可将每个节点作为代理地址使用,扩展代理服务器的功能和灵活性。
- [ ] **域名/IP黑白名单**:提供目标域名或IP的黑白名单配置功能,类似VPN规则模式,实现伪全局代理效果,确保特定流量按需转发。
- [ ] **版本自动升级**:内置版本自动升级模块,确保软件始终运行在最新版本,减少手动维护的工作量,同时提升安全性与兼容性。
如果您有好的创意,或在使用过程中遇到bug,请通过以下方式联系作者反馈!
微信公众号:**樱花庄的本间白猫**
## 特别鸣谢
本排名不分先后,感谢为本项目提供帮助的师傅们。
- [AabyssZG (曾哥)](https://github.com/AabyssZG)
- [ProbiusOfficial (探姬)](https://github.com/ProbiusOfficial)
- [gh0stkey (EvilChen)](https://github.com/gh0stkey)
- [huangzheng2016(HydrogenE7)](https://github.com/huangzheng2016)
- chars6
- qianzai(千载)
- ziwindlu
- yuzhegan
- 摘星怪
## 赞助开源
开源不易,如果您觉得工具不错,或许可以试着赞助一下作者的开发哦~
---
| 排名 | ID | 赞助金额(元) |
| :--: | :-----------------: | :------------: |
| 1 | **陆沉** | 1355.5 |
| 2 | **乡村牛公子** | 976 |
| 3 | **柯林斯.民间新秀** | 696 |
| [赞助榜单](https://github.com/honmashironeko/Thanks-for-sponsorship) | 您的每一份赞助都是作者源源不断的动力! | (´∀`)♡ |
---

## 代理推荐
- [第一家便宜大碗代理购买,用邀请码注册得5000免费IP+10元优惠券](https://h.shanchendaili.com/invite_reg.html?invite=fM6fVG)
- [各大运营商流量卡](https://172.lot-ml.com/ProductEn/Index/0b7c9adef5e9648f)
- [国外匿名代理](https://www.ipmart.io?source=Shironeko)

================================================
FILE: app.py
================================================
from flask import Flask, render_template, jsonify, request, redirect, url_for, send_from_directory
import sys
import os
import logging
from datetime import datetime
import json
from configparser import ConfigParser
from itertools import cycle
import werkzeug.serving
from functools import wraps
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ProxyCat import run_server
from modules.modules import load_config, check_proxies, get_message, load_ip_list
from modules.proxyserver import AsyncProxyServer
import asyncio
import threading
import time
app = Flask(__name__,
template_folder='web/templates',
static_folder='web/static')
werkzeug.serving.WSGIRequestHandler.log = lambda self, type, message, *args: None
logging.getLogger('werkzeug').setLevel(logging.ERROR)
config = load_config('config/config.ini')
server = AsyncProxyServer(config)
def get_config_path(filename):
return os.path.join('config', filename)
log_file = 'logs/proxycat.log'
os.makedirs('logs', exist_ok=True)
log_messages = []
max_log_messages = 10000
class CustomFormatter(logging.Formatter):
def formatTime(self, record, datefmt=None):
return datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S')
def setup_logging():
file_formatter = CustomFormatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setFormatter(file_formatter)
console_handler = logging.StreamHandler()
console_formatter = CustomFormatter('%(asctime)s - %(levelname)s - %(message)s')
console_handler.setFormatter(console_formatter)
memory_handler = MemoryHandler()
memory_handler.setFormatter(CustomFormatter('%(message)s'))
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
root_logger.addHandler(file_handler)
root_logger.addHandler(console_handler)
root_logger.addHandler(memory_handler)
class MemoryHandler(logging.Handler):
def emit(self, record):
global log_messages
log_messages.append({
'time': datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S'),
'level': record.levelname,
'message': self.format(record)
})
if len(log_messages) > max_log_messages:
log_messages = log_messages[-max_log_messages:]
def require_token(f):
@wraps(f)
def decorated_function(*args, **kwargs):
token = request.args.get('token')
config_token = server.config.get('token', '')
if not config_token:
return f(*args, **kwargs)
if not token or token != config_token:
return jsonify({
'status': 'error',
'message': get_message('invalid_token', server.language)
}), 401
return f(*args, **kwargs)
return decorated_function
@app.route('/')
def root():
token = request.args.get('token')
if token:
return redirect(f'/web?token={token}')
return redirect('/web')
@app.route('/web')
@require_token
def web():
return render_template('index.html')
@app.route('/api/status')
@require_token
def get_status():
with open('config/config.ini', 'r', encoding='utf-8') as f:
config_content = f.read()
config = ConfigParser()
config.read('config/config.ini', encoding='utf-8')
server_config = dict(config.items('Server')) if config.has_section('Server') else {}
current_proxy = server.current_proxy
if not current_proxy and not server.use_getip:
if hasattr(server, 'proxies') and server.proxies:
current_proxy = server.proxies[0]
else:
current_proxy = get_message('no_proxy', server.language)
time_left = server.time_until_next_switch()
if server.mode == 'loadbalance':
if time_left == float('inf'):
time_left = -1
return jsonify({
'current_proxy': current_proxy,
'mode': server.mode,
'port': int(server_config.get('port', '1080')),
'interval': server.interval,
'time_left': time_left,
'total_proxies': len(server.proxies) if hasattr(server, 'proxies') else 0,
'use_getip': server.use_getip,
'getip_url': getattr(server, 'getip_url', '') if getattr(server, 'use_getip', False) else '',
'auth_required': server.auth_required,
'display_level': int(server_config.get('display_level', '1')),
'service_status': 'running' if server.running else 'stopped',
'config': server_config
})
@app.route('/api/config', methods=['POST'])
def save_config():
try:
new_config = request.get_json()
current_config = load_config('config/config.ini')
port_changed = str(new_config.get('port', '')) != str(current_config.get('port', ''))
mode_changed = new_config.get('mode', '') != current_config.get('mode', '')
use_getip_changed = (new_config.get('use_getip', 'False').lower() == 'true') != (current_config.get('use_getip', 'False').lower() == 'true')
config_parser = ConfigParser()
config_parser.read('config/config.ini', encoding='utf-8')
if not config_parser.has_section('Server'):
config_parser.add_section('Server')
for key, value in new_config.items():
if key != 'users':
config_parser.set('Server', key, str(value))
with open('config/config.ini', 'w', encoding='utf-8') as f:
config_parser.write(f)
old_mode = server.mode
old_use_getip = server.use_getip
server.config = load_config('config/config.ini')
server._init_config_values(server.config)
if mode_changed or use_getip_changed:
server._handle_mode_change()
if new_config.get('mode') == 'loadbalance':
server.last_switch_time = time.time()
server.last_switch_attempt = 0
return jsonify({
'status': 'success',
'port_changed': port_changed,
'service_status': 'running' if server.running else 'stopped'
})
except Exception as e:
logging.error(f"Error saving config: {e}")
return jsonify({
'status': 'error',
'message': str(e)
})
@app.route('/api/proxies', methods=['GET', 'POST'])
def handle_proxies():
if request.method == 'POST':
try:
proxies = request.json.get('proxies', [])
proxy_file = get_config_path(os.path.basename(server.proxy_file))
with open(proxy_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(proxies))
server.proxies = server._load_file_proxies()
if server.proxies:
server.proxy_cycle = cycle(server.proxies)
server.current_proxy = next(server.proxy_cycle)
return jsonify({
'status': 'success',
'message': get_message('proxy_save_success', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('proxy_save_failed', server.language, str(e))
})
else:
try:
proxy_file = get_config_path(os.path.basename(server.proxy_file))
with open(proxy_file, 'r', encoding='utf-8') as f:
proxies = f.read().splitlines()
return jsonify({'proxies': proxies})
except Exception:
return jsonify({'proxies': []})
@app.route('/api/check_proxies')
def check_proxies_api():
try:
test_url = request.args.get('test_url', 'https://www.baidu.com')
valid_proxies = asyncio.run(check_proxies(server.proxies, test_url))
total_valid = len(valid_proxies)
return jsonify({
'status': 'success',
'valid_proxies': valid_proxies,
'total': total_valid,
'message': get_message('proxy_check_result', server.language, total_valid)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('proxy_check_failed', server.language, str(e))
})
@app.route('/api/ip_lists', methods=['GET', 'POST'])
def handle_ip_lists():
if request.method == 'POST':
try:
list_type = request.json.get('type')
ip_list = request.json.get('list', [])
base_filename = os.path.basename(server.whitelist_file if list_type == 'whitelist' else server.blacklist_file)
filename = get_config_path(base_filename)
with open(filename, 'w', encoding='utf-8') as f:
f.write('\n'.join(ip_list))
if list_type == 'whitelist':
server.whitelist = load_ip_list(filename)
else:
server.blacklist = load_ip_list(filename)
return jsonify({
'status': 'success',
'message': get_message('ip_list_save_success', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('ip_list_save_failed', server.language, str(e))
})
else:
whitelist_file = get_config_path(os.path.basename(server.whitelist_file))
blacklist_file = get_config_path(os.path.basename(server.blacklist_file))
return jsonify({
'whitelist': list(load_ip_list(whitelist_file)),
'blacklist': list(load_ip_list(blacklist_file))
})
@app.route('/api/logs')
def get_logs():
try:
start = int(request.args.get('start', 0))
limit = int(request.args.get('limit', 100))
level = request.args.get('level', 'ALL')
search = request.args.get('search', '').lower()
filtered_logs = log_messages
if level != 'ALL':
filtered_logs = [log for log in log_messages if log['level'] == level]
if search:
filtered_logs = [
log for log in filtered_logs
if search in log['message'].lower() or
search in log['level'].lower() or
search in log['time'].lower()
]
return jsonify({
'logs': filtered_logs[start:start+limit],
'total': len(filtered_logs),
'status': 'success'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
})
@app.route('/api/logs/clear', methods=['POST'])
def clear_logs():
try:
global log_messages
log_messages = []
with open(log_file, 'w', encoding='utf-8') as f:
f.write('')
return jsonify({
'status': 'success',
'message': get_message('logs_cleared', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('clear_logs_failed', server.language, str(e))
})
@app.route('/api/switch_proxy')
@require_token
def switch_proxy():
try:
result = asyncio.run(server.switch_proxy())
if result:
return jsonify({
'status': 'success',
'current_proxy': server.current_proxy,
'message': get_message('switch_success', server.language)
})
else:
return jsonify({
'status': 'error',
'message': get_message('switch_failed', server.language, 'Proxy switch not needed or in cooldown')
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('switch_failed', server.language, str(e))
})
@app.route('/api/service', methods=['POST'])
@require_token
def control_service():
try:
action = request.json.get('action')
if action == 'start':
if not server.running:
server.stop_server = False
if hasattr(server, 'proxy_thread') and server.proxy_thread and server.proxy_thread.is_alive():
server.proxy_thread.join(timeout=5)
server.proxy_thread = threading.Thread(target=lambda: asyncio.run(run_server(server)), daemon=True)
server.proxy_thread.start()
for _ in range(10):
if server.running:
break
time.sleep(0.5)
if server.running:
return jsonify({
'status': 'success',
'message': get_message('service_start_success', server.language),
'service_status': 'running'
})
else:
return jsonify({
'status': 'error',
'message': get_message('service_start_failed', server.language),
'service_status': 'stopped'
})
return jsonify({
'status': 'success',
'message': get_message('service_already_running', server.language),
'service_status': 'running'
})
elif action == 'stop':
if server.running:
server.stop_server = True
server.running = False
if server.server_instance:
server.server_instance.close()
if hasattr(server, 'proxy_thread') and server.proxy_thread:
server.proxy_thread = None
for _ in range(5):
if server.server_instance is None:
break
time.sleep(0.2)
return jsonify({
'status': 'success',
'message': get_message('service_stop_success', server.language),
'service_status': 'stopped'
})
return jsonify({
'status': 'success',
'message': get_message('service_not_running', server.language),
'service_status': 'stopped'
})
elif action == 'restart':
if server.running:
server.stop_server = True
if server.server_instance:
server.server_instance.close()
for _ in range(10):
if not server.running:
break
time.sleep(0.5)
if server.running:
if hasattr(server, 'proxy_thread') and server.proxy_thread:
server.proxy_thread = None
server.running = False
server.stop_server = False
server.proxy_thread = threading.Thread(target=lambda: asyncio.run(run_server(server)), daemon=True)
server.proxy_thread.start()
for _ in range(10):
if server.running:
break
time.sleep(0.5)
if server.running:
return jsonify({
'status': 'success',
'message': get_message('service_restart_success', server.language)
})
else:
return jsonify({
'status': 'error',
'message': get_message('service_restart_failed', server.language)
})
return jsonify({
'status': 'error',
'message': get_message('invalid_action', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('operation_failed', server.language, str(e))
})
@app.route('/api/language', methods=['POST'])
def change_language():
try:
new_language = request.json.get('language', 'cn')
if new_language not in ['cn', 'en']:
return jsonify({
'status': 'error',
'message': get_message('unsupported_language', server.language)
})
config = ConfigParser()
config.read('config/config.ini', encoding='utf-8')
if 'Server' not in config:
config.add_section('Server')
config.set('Server', 'language', new_language)
with open('config/config.ini', 'w', encoding='utf-8') as f:
config.write(f)
server.language = new_language
return jsonify({
'status': 'success',
'language': new_language
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('operation_failed', server.language, str(e))
})
@app.route('/api/version')
def check_version():
try:
import re
import httpx
from packaging import version
import logging
httpx_logger = logging.getLogger('httpx')
original_level = httpx_logger.level
httpx_logger.setLevel(logging.WARNING)
CURRENT_VERSION = "ProxyCat-V2.0.4"
try:
client = httpx.Client(transport=httpx.HTTPTransport(retries=3))
response = client.get("https://y.shironekosan.cn/1.html", timeout=10)
response.raise_for_status()
content = response.text
match = re.search(r'<p>(ProxyCat-V\d+\.\d+\.\d+)</p>', content)
if match:
latest_version = match.group(1)
is_latest = version.parse(latest_version.split('-V')[1]) <= version.parse(CURRENT_VERSION.split('-V')[1])
return jsonify({
'status': 'success',
'is_latest': is_latest,
'current_version': CURRENT_VERSION,
'latest_version': latest_version
})
else:
return jsonify({
'status': 'error',
'message': get_message('version_info_not_found', server.language)
})
finally:
httpx_logger.setLevel(original_level)
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('update_check_error', server.language, str(e))
})
@app.route('/api/users', methods=['GET', 'POST'])
@require_token
def handle_users():
if request.method == 'POST':
try:
users = request.json.get('users', {})
config = ConfigParser()
config.read('config/config.ini', encoding='utf-8')
sections_to_preserve = {}
for section in config.sections():
if section != 'Users':
sections_to_preserve[section] = dict(config.items(section))
config = ConfigParser()
for section, options in sections_to_preserve.items():
config.add_section(section)
for key, value in options.items():
config.set(section, key, value)
if users:
config.add_section('Users')
for username, password in users.items():
config.set('Users', username, password)
with open('config/config.ini', 'w', encoding='utf-8') as f:
config.write(f)
server.users = users
server.auth_required = bool(users)
if hasattr(server, 'proxy_server') and server.proxy_server:
server.proxy_server.users = users
server.proxy_server.auth_required = bool(users)
return jsonify({
'status': 'success',
'message': get_message('users_save_success', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('users_save_failed', server.language, str(e))
})
else:
try:
config = ConfigParser()
config.read('config/config.ini', encoding='utf-8')
users = {}
if config.has_section('Users'):
users = dict(config.items('Users'))
return jsonify({'users': users})
except Exception as e:
logging.error(f"Error getting users: {e}")
return jsonify({'users': {}})
@app.route('/static/<path:path>')
def send_static(path):
return send_from_directory('web/static', path)
def run_proxy_server():
try:
asyncio.run(run_server(server))
except KeyboardInterrupt:
logging.info(get_message('user_interrupt', server.language))
except Exception as e:
logging.error(f"Proxy server error: {e}")
if __name__ == '__main__':
setup_logging()
web_port = int(config.get('web_port', '5000'))
web_url = f"http://127.0.0.1:{web_port}"
if config.get('token'):
web_url += f"/web?token={config.get('token')}"
logging.info(get_message('web_panel_url', server.language, web_url))
logging.info(get_message('web_panel_notice', server.language))
proxy_thread = threading.Thread(target=run_proxy_server, daemon=True)
proxy_thread.start()
app.run(host='0.0.0.0', port=web_port)
================================================
FILE: config/blacklist.txt
================================================
================================================
FILE: config/config.ini
================================================
[Server]
display_level = 1
port = 1080
web_port = 5000
mode = cycle
interval = 300
use_getip = false
getip_url = http://example.com/getip
proxy_username =
proxy_password =
proxy_file = ip.txt
check_proxies = true
test_url =
language = cn
whitelist_file = whitelist.txt
blacklist_file = blacklist.txt
ip_auth_priority = whitelist
token = honmashironeko
[Users]
neko = 123456
k = 123
================================================
FILE: config/getip.py
================================================
from modules.modules import get_message, load_config
import requests
import time
def newip():
config = load_config()
language = config.get('language', 'cn')
def handle_error(error_type, details=None):
error_msg = 'whitelist_error' if error_type == 'whitelist' else 'proxy_file_not_found'
print(get_message(error_msg, language, str(details)))
raise ValueError(f"{error_type}: {details}")
try:
url = config.get('getip_url', '')
username = config.get('proxy_username', '')
password = config.get('proxy_password', '')
if not url:
raise ValueError('getip_url')
def get_proxy():
response = requests.get(url)
response.raise_for_status()
return response.text.split("\r\n")[0]
proxy = get_proxy()
if proxy == "error000x-13":
appKey = ""
anquanma = ""
whitelist_url = f"https://sch.shanchendaili.com/api.html?action=addWhiteList&appKey={appKey}&anquanma={anquanma}"
requests.get(whitelist_url).raise_for_status()
time.sleep(1)
proxy = get_proxy()
if username and password:
return f"socks5://{username}:{password}@{proxy}"
return f"socks5://{proxy}"
except requests.RequestException as e:
handle_error('request', e)
except ValueError as e:
handle_error('config', e)
except Exception as e:
handle_error('unknown', e)
================================================
FILE: config/ip.txt
================================================
http://127.0.0.1:7890
socks5://127.0.0.1:7890
================================================
FILE: config/whitelist.txt
================================================
================================================
FILE: docker-compose.yml
================================================
version: '3'
services:
proxycat:
build: .
environment:
- TZ=Asia/Shanghai
ports:
- "1080:1080"
- "5000:5000"
volumes:
- ./config:/app/config
restart: unless-stopped
network_mode: "bridge"
================================================
FILE: logs/proxycat.log
================================================
================================================
FILE: modules/modules.py
================================================
import asyncio, logging, random, httpx, re, os, time
from configparser import ConfigParser
from packaging import version
from colorama import Fore, Style
class ColoredFormatter(logging.Formatter):
COLORS = {
logging.INFO: Fore.GREEN,
logging.WARNING: Fore.YELLOW,
logging.ERROR: Fore.RED,
logging.CRITICAL: Fore.RED + Style.BRIGHT,
}
def format(self, record):
log_color = self.COLORS.get(record.levelno, Fore.WHITE)
record.msg = f"{log_color}{record.msg}{Style.RESET_ALL}"
return super().format(record)
MESSAGES = {
'cn': {
'getting_new_proxy': '正在获取新的代理IP',
'new_proxy_is': '新的代理IP为: {}',
'proxy_check_start': '开始检测代理地址...',
'proxy_check_disabled': '代理检测已禁用',
'valid_proxies': '有效代理地址: {}',
'no_valid_proxies': '没有有效的代理地址',
'proxy_check_failed': '{}代理 {} 检测失败: {}',
'proxy_switch': '切换代理: {} -> {}',
'proxy_consecutive_fails': '代理 {} 连续失败 {} 次,正在切换新代理',
'proxy_invalid': '代理 {} 无效,立即切换代理',
'proxy_failure': '代理检测失败: {}',
'proxy_failure_threshold': '代理无效,开始切换',
'proxy_check_error': '代理检查时发生错误: {}',
'connection_timeout': '连接超时',
'data_transfer_timeout': '数据传输超时,正在重试...',
'connection_reset': '连接被重置',
'transfer_cancelled': '传输被取消',
'data_transfer_error': '数据传输错误: {}',
'unsupported_protocol': '不支持的协议请求: {}',
'client_error': '客户端处理出错: {}',
'response_write_error': '响应写入错误: {}',
'server_closing': '服务器正在关闭...',
'program_interrupted': '程序被用户中断',
'multiple_proxy_fail': '多次尝试获取有效代理失败,退出程序',
'current_proxy': '当前代理',
'next_switch': '下次切换',
'seconds': '秒',
'no_proxies_available': '没有可用的代理',
'proxy_file_not_found': '代理文件不存在: {}',
'auth_not_set': '未设置 (无需认证)',
'public_account': '公众号',
'blog': '博客',
'proxy_mode': '代理轮换模式',
'cycle': '循环',
'loadbalance': '负载均衡',
'single_round': '单轮',
'proxy_interval': '代理更换时间',
'default_auth': '默认账号密码',
'local_http': '本地监听地址 (HTTP)',
'local_socks5': '本地监听地址 (SOCKS5)',
'star_project': '开源项目求 Star',
'client_handle_error': '客户端处理错误: {}',
'proxy_invalid_switch': '代理无效,切换代理',
'request_fail_retry': '请求失败,重试剩余次数: {}',
'user_interrupt': '用户中断程序',
'new_version_found': '发现新版本!',
'visit_quark': '夸克网盘: https://pan.quark.cn/s/39b4b5674570',
'visit_github': 'GitHub: https://github.com/honmashironeko/ProxyCat',
'visit_baidu': '百度网盘: https://pan.baidu.com/s/1C9LVC9aiaQeYFSj_2mWH1w?pwd=13r5',
'latest_version': '当前已是最新版本',
'version_info_not_found': '未找到版本信息',
'update_check_error': '检查更新失败: {}',
'unauthorized_ip': '未授权的IP尝试访问: {}',
'client_cancelled': '客户端连接已取消',
'socks5_connection_error': 'SOCKS5连接错误: {}',
'connect_timeout': '连接超时',
'connection_reset': '连接被重置',
'transfer_cancelled': '传输已取消',
'client_request_error': '客户端请求错误: {}',
'unsupported_protocol': '不支持的协议: {}',
'request_retry': '请求失败,重试中 (剩余{}次)',
'response_write_error': '写入响应时出错: {}',
'consecutive_failures': '检测到连续代理失败: {}',
'invalid_proxy': '当前代理无效: {}',
'whitelist_error': '添加白名单失败: {}',
'api_mode_notice': '当前为API模式,收到请求将自动获取代理地址',
'server_running': '代理服务器运行在 {}:{}',
'server_start_error': '服务器启动错误: {}',
'server_shutting_down': '正在关闭服务器...',
'client_process_error': '处理客户端请求时出错: {}',
'request_handling_error': '请求处理错误: {}',
'proxy_forward_error': '代理转发错误: {}',
'data_transfer_timeout': '{}数据传输超时',
'data_transfer_error': '{}数据传输错误: {}',
'status_update_error': '状态更新出错',
'display_level_notice': '当前显示级别: {}',
'display_level_desc': '''显示级别说明:
0: 仅显示代理切换和错误信息
1: 显示代理切换、倒计时和错误信息
2: 显示所有详细信息''',
'new_client_connect': '新客户端连接 - IP: {}, 用户: {}',
'no_auth': '无认证',
'connection_error': '连接处理错误: {}',
'cleanup_error': '清理IP错误: {}',
'port_changed': '端口已更改: {} -> {},需要重启服务器生效',
'config_updated': '服务器配置已更新',
'load_proxy_file_error': '加载代理文件失败: {}',
'proxy_check_result': '代理检查完成,有效代理:{}个',
'no_proxy': '无代理',
'cycle_mode': '循环模式',
'loadbalance_mode': '负载均衡模式',
'proxy_check_start': '开始检查代理...',
'proxy_check_complete': '代理检查完成',
'proxy_save_success': '代理保存成功',
'proxy_save_failed': '代理保存失败: {}',
'ip_list_save_success': 'IP名单保存成功',
'ip_list_save_failed': 'IP名单保存失败: {}',
'switch_success': '代理切换成功',
'switch_failed': '代理切换失败: {}',
'service_start_success': '服务启动成功',
'service_start_failed': '服务启动失败',
'service_already_running': '服务已在运行',
'service_stop_success': '服务停止成功',
'service_not_running': '服务未在运行',
'service_restart_success': '服务重启成功',
'service_restart_failed': '服务重启失败',
'invalid_action': '无效的操作',
'operation_failed': '操作失败: {}',
'logs_cleared': '日志已清除',
'clear_logs_failed': '清除日志失败: {}',
'unsupported_language': '不支持的语言',
'language_changed': '语言已切换为{}',
'loading': '加载中...',
'get_proxy_failed': '获取新代理失败: {}',
'log_level_all': '全部',
'log_level_info': '信息',
'log_level_warning': '警告',
'log_level_error': '错误',
'log_level_critical': '严重错误',
'confirm_clear_logs': '确定要清除所有日志吗?此操作不可恢复。',
'language_label': '语言',
'chinese': '中文',
'english': 'English',
'manual_switch_btn': '手动切换',
'service_control_title': '服务控制',
'language_switch_success': '',
'language_switch_failed': '',
'refresh_failed': '刷新数据失败: {}',
'auth_username_label': '认证用户名',
'auth_password_label': '认证密码',
'proxy_auth_username_label': '代理认证用户名',
'proxy_auth_password_label': '代理认证密码',
'progress_bar_label': '切换进度',
'proxy_settings_title': '代理设置',
'config_save_success': '配置保存成功',
'config_save_failed': '配置保存失败:{}',
'config_restart_required': '配置已更改,需要重启服务器生效',
'confirm_restart_service': '是否立即重启服务器?',
'service_status': '服务状态',
'running': '运行中',
'stopped': '已停止',
'restarting': '重启中',
'unknown': '未知',
'service_start_failed': '服务启动失败:{}',
'service_stop_failed': '服务停止失败:{}',
'service_restart_failed': '服务重启失败:{}',
'invalid_token': '无效的访问令牌',
'config_file_changed': '检测到配置文件更改,正在重新加载...',
'proxy_file_changed': '代理文件已更改,正在重新加载...',
'test_target_label': '测试目标地址',
'invalid_test_target': '无效的测试目标地址',
'users_save_success': '用户保存成功',
'users_save_failed': '用户保存失败:{}',
'user_management_title': '用户管理',
'username_column': '用户名',
'password_column': '密码',
'actions_column': '操作',
'add_user_btn': '添加用户',
'enter_username': '请输入用户名',
'enter_password': '请输入密码',
'confirm_delete_user': '确定要删除该用户吗?',
'no_logs_found': '未找到匹配的日志',
'clear_search': '清除搜索',
'web_panel_url': '网页控制面板地址: {}',
'web_panel_notice': '请使用浏览器访问上述地址来管理代理服务器',
'api_proxy_settings_title': 'API代理设置',
'all_retries_failed': '所有重试均已失败,最后错误: {}',
'proxy_get_failed': '获取代理失败',
'proxy_get_error': '获取代理错误: {}',
'request_error': '请求错误: {}',
'proxy_switch_error': '代理切换错误: {}',
},
'en': {
'getting_new_proxy': 'Getting new proxy IP',
'new_proxy_is': 'New proxy IP is: {}',
'proxy_check_start': 'Starting proxy check...',
'proxy_check_disabled': 'Proxy check is disabled',
'valid_proxies': 'Valid proxies: {}',
'no_valid_proxies': 'No valid proxies found',
'proxy_check_failed': '{} proxy {} check failed: {}',
'proxy_switch': 'Switch proxy: {} -> {}',
'proxy_consecutive_fails': 'Proxy {} failed {} times consecutively, switching to new proxy',
'proxy_invalid': 'Proxy {} is invalid, switching proxy immediately',
'proxy_failure': 'Proxy check failed: {}',
'proxy_failure_threshold': 'Proxy invalid, switching now',
'proxy_check_error': 'Error occurred during proxy check: {}',
'connection_timeout': 'Connection timeout',
'data_transfer_timeout': 'Data transfer timeout, retrying...',
'connection_reset': 'Connection reset',
'transfer_cancelled': 'Transfer cancelled',
'data_transfer_error': 'Data transfer error: {}',
'unsupported_protocol': 'Unsupported protocol request: {}',
'client_error': 'Client handling error: {}',
'response_write_error': 'Response write error: {}',
'server_closing': 'Server is closing...',
'program_interrupted': 'Program interrupted by user',
'multiple_proxy_fail': 'Multiple attempts to get valid proxy failed, exiting',
'current_proxy': 'Current Proxy',
'next_switch': 'Next Switch',
'seconds': 's',
'no_proxies_available': 'No proxies available',
'proxy_file_not_found': 'Proxy file not found: {}',
'auth_not_set': 'Not set (No authentication required)',
'public_account': 'WeChat Public Number',
'blog': 'Blog',
'proxy_mode': 'Proxy Rotation Mode',
'cycle': 'Cycle',
'loadbalance': 'Load Balance',
'single_round': 'Single Round',
'proxy_interval': 'Proxy Change Interval',
'default_auth': 'Default Username and Password',
'local_http': 'Local Listening Address (HTTP)',
'local_socks5': 'Local Listening Address (SOCKS5)',
'star_project': 'Star the Project',
'client_handle_error': 'Client handling error: {}',
'proxy_invalid_switch': 'Proxy invalid, switching proxy',
'request_fail_retry': 'Request failed, retrying remaining times: {}',
'user_interrupt': 'User interrupted the program',
'new_version_found': 'New version available!',
'visit_quark': 'Quark Drive: https://pan.quark.cn/s/39b4b5674570',
'visit_github': 'GitHub: https://github.com/honmashironeko/ProxyCat',
'visit_baidu': 'Baidu Drive: https://pan.baidu.com/s/1C9LVC9aiaQeYFSj_2mWH1w?pwd=13r5',
'latest_version': 'You are using the latest version',
'version_info_not_found': 'Version information not found',
'update_check_error': 'Failed to check for updates: {}',
'unauthorized_ip': 'Unauthorized IP attempt: {}',
'client_cancelled': 'Client connection cancelled',
'socks5_connection_error': 'SOCKS5 connection error: {}',
'connect_timeout': 'Connection timeout',
'connection_reset': 'Connection reset',
'transfer_cancelled': 'Transfer cancelled',
'data_transfer_error': 'Data transfer error: {}',
'client_request_error': 'Client request handling error: {}',
'unsupported_protocol': 'Unsupported protocol: {}',
'request_retry': 'Request failed, retrying ({} left)',
'request_error': 'Error during request: {}',
'response_write_error': 'Error writing response: {}',
'consecutive_failures': 'Consecutive proxy failures detected for {}',
'invalid_proxy': 'Current proxy is invalid: {}',
'whitelist_error': 'Failed to add whitelist: {}',
'api_mode_notice': 'Currently in API mode, proxy address will be automatically obtained upon request',
'all_retries_failed': 'All retries failed, last error: {}',
'proxy_get_failed': 'Failed to get proxy',
'proxy_get_error': 'Error getting proxy: {}',
'proxy_switch_error': 'Error switching proxy: {}',
'server_running': 'Proxy server running at {}:{}',
'server_start_error': 'Server startup error: {}',
'server_shutting_down': 'Server shutting down...',
'client_process_error': 'Client processing error: {}',
'request_handling_error': 'Request handling error: {}',
'proxy_forward_error': 'Proxy forwarding error: {}',
'data_transfer_timeout': '{}data transfer timeout',
'status_update_error': 'Status update error',
'display_level_notice': 'Current display level: {}',
'display_level_desc': '''Display level description:
0: Only show proxy switch and error messages
1: Show proxy switch, countdown and error messages
2: Show all detailed information''',
'new_client_connect': 'New client connection - IP: {}, User: {}',
'no_auth': 'No authentication',
'connection_error': 'Connection handling error: {}',
'cleanup_error': 'Cleanup IP error: {}',
'port_changed': 'Port changed: {} -> {}, server restart required to take effect',
'config_updated': 'Server configuration updated',
'load_proxy_file_error': 'Failed to load proxy file: {}',
'proxy_check_result': 'Proxy check completed, valid proxies: {}',
'no_proxy': 'No proxy',
'cycle_mode': 'Cycle mode',
'loadbalance_mode': 'Load balance mode',
'proxy_check_start': 'Starting proxy check...',
'proxy_check_complete': 'Proxy check completed',
'proxy_save_success': 'Proxy saved successfully',
'proxy_save_failed': 'Failed to save proxy: {}',
'ip_list_save_success': 'IP list saved successfully',
'ip_list_save_failed': 'Failed to save IP list: {}',
'switch_success': 'Proxy switched successfully',
'switch_failed': 'Failed to switch proxy: {}',
'service_start_success': 'Service started successfully',
'service_start_failed': 'Failed to start service',
'service_already_running': 'Service is already running',
'service_stop_success': 'Service stopped successfully',
'service_not_running': 'Service is not running',
'service_restart_success': 'Service restarted successfully',
'service_restart_failed': 'Failed to restart service',
'invalid_action': 'Invalid action',
'operation_failed': 'Operation failed: {}',
'logs_cleared': 'Logs cleared',
'clear_logs_failed': 'Failed to clear logs: {}',
'unsupported_language': 'Unsupported language',
'language_changed': 'Language changed to {}',
'loading': 'Loading...',
'get_proxy_failed': 'Failed to get new proxy: {}',
'log_level_all': 'All',
'log_level_info': 'Info',
'log_level_warning': 'Warning',
'log_level_error': 'Error',
'log_level_critical': 'Critical',
'confirm_clear_logs': 'Are you sure you want to clear all logs? This action cannot be undone.',
'language_label': 'Language',
'chinese': 'Chinese',
'english': 'English',
'manual_switch_btn': 'Switch Manually',
'service_control_title': 'Service Control',
'language_switch_success': 'Language switched successfully',
'language_switch_failed': 'Failed to switch language',
'refresh_failed': 'Failed to refresh data: {}',
'auth_username_label': 'Authentication Username',
'auth_password_label': 'Authentication Password',
'proxy_auth_username_label': 'Proxy Authentication Username',
'proxy_auth_password_label': 'Proxy Authentication Password',
'progress_bar_label': 'Switch Progress',
'proxy_settings_title': 'Proxy Settings',
'config_save_success': 'Configuration saved successfully',
'config_save_failed': 'Failed to save configuration: {}',
'config_restart_required': 'Configuration changed, server restart required to take effect',
'confirm_restart_service': 'Restart server now?',
'service_status': 'Service Status',
'running': 'Running',
'stopped': 'Stopped',
'restarting': 'Restarting',
'unknown': 'Unknown',
'service_start_failed': 'Failed to start service: {}',
'service_stop_failed': 'Failed to stop service: {}',
'service_restart_failed': 'Failed to restart service: {}',
'invalid_token': 'Invalid access token',
'config_file_changed': 'Config file change detected, reloading...',
'proxy_file_changed': 'Proxy file changed, reloading...',
'test_target_label': 'Test Target Address',
'invalid_test_target': 'Invalid test target address',
'users_save_success': 'Users saved successfully',
'users_save_failed': 'Failed to save users: {}',
'user_management_title': 'User Management',
'username_column': 'Username',
'password_column': 'Password',
'actions_column': 'Actions',
'add_user_btn': 'Add User',
'enter_username': 'Please enter username',
'enter_password': 'Please enter password',
'confirm_delete_user': 'Are you sure you want to delete this user?',
'no_logs_found': 'No matching logs found',
'clear_search': 'Clear Search',
'web_panel_url': 'Web panel URL: {}',
'web_panel_notice': 'Please use a browser to access the above URL to manage the proxy server',
'api_proxy_settings_title': 'API Proxy Settings',
}
}
class MessageManager:
def __init__(self, messages=MESSAGES):
self.messages = messages
self.default_lang = 'cn'
def get(self, key, lang='cn', *args):
try:
return self.messages[lang][key].format(*args) if args else self.messages[lang][key]
except KeyError:
return self.messages[self.default_lang][key] if key in self.messages[self.default_lang] else key
message_manager = MessageManager(MESSAGES)
get_message = message_manager.get
def print_banner(config):
language = config.get('language', 'cn').lower()
has_auth = config.get('username') and config.get('password')
auth_info = f"{config.get('username')}:{config.get('password')}" if has_auth else get_message('auth_not_set', language)
http_addr = f"http://{auth_info}@127.0.0.1:{config.get('port')}" if has_auth else f"http://127.0.0.1:{config.get('port')}"
socks5_addr = f"socks5://{auth_info}@127.0.0.1:{config.get('port')}" if has_auth else f"socks5://127.0.0.1:{config.get('port')}"
banner_info = [
(get_message('public_account', language), '樱花庄的本间白猫'),
(get_message('blog', language), 'https://y.shironekosan.cn'),
(get_message('proxy_mode', language), get_message('cycle', language) if config.get('mode') == 'cycle' else get_message('loadbalance', language) if config.get('mode') == 'loadbalance' else get_message('single_round', language)),
(get_message('proxy_interval', language), f"{config.get('interval')}{get_message('seconds', language)}"),
(get_message('default_auth', language), auth_info),
(get_message('local_http', language), http_addr),
(get_message('local_socks5', language), socks5_addr),
(get_message('star_project', language), 'https://github.com/honmashironeko/ProxyCat'),
]
print(f"{Fore.MAGENTA}{'=' * 55}")
for key, value in banner_info:
print(f"{Fore.YELLOW}{key}: {Fore.GREEN}{value}{Style.RESET_ALL}")
print(f"{Fore.MAGENTA}{'=' * 55}\n")
display_level = config.get('display_level', '1')
if int(display_level) >= 2:
print(f"\n{Fore.CYAN}{get_message('display_level_desc', language)}{Style.RESET_ALL}")
else:
print(f"\n{Fore.CYAN}{get_message('display_level_notice', language).format(display_level)}{Style.RESET_ALL}")
logo1 = r"""
|\ _,,,---,,_ by 本间白猫
ZZZzz /,`.-'`' -. ;-;;,_
|,4- ) )-,_. ,\ ( `'-'
'---''(_/--' `-'\_) ProxyCat
"""
logo2 = r"""
* ,MMM8&&&. *
MMMM88&&&&& .
MMMM88&&&&&&&
* MMM88&&&&&&&&
MMM88&&&&&&&&
'MMM88&&&&&&'
'MMM8&&&' *
/\/|_ __/\\
/ -\ /- ~\ . '
\ =_YT_ = /
/==*(` `\ ~ \ ProxyCat
/ \ / `\ by 本间白猫
| | ) ~ (
/ \ / ~ \\
\ / \~ ~/
_/\_/\_/\__ _/_/\_/\__~__/_/\_/\_/\_/\_/\_
| | | | ) ) | | | (( | | | | | |
| | | |( ( | | | \\ | | | | | |
| | | | )_) | | | |))| | | | | |
| | | | | | | | (/ | | | | | |
| | | | | | | | | | | | | | |
"""
logo3 = r"""
/\_/\ _
/`` \ / )
|n n |__ ( (
=(Y =.'` `\ \ \\
{`"` \ ) )
{ / |/ /
\\ ,( / /
ProxyCat) ) /-'\ ,_.' by 本间白猫
(,(,/ ((,,/
"""
logo4 = r"""
.-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)
DEFAULT_CONFIG = {
'port': '1080',
'mode': 'cycle',
'interval': '300',
'username': 'neko',
'password': '123456',
'use_getip': 'False',
'proxy_file': 'ip.txt',
'check_proxies': 'True',
'whitelist_file': '',
'blacklist_file': '',
'ip_auth_priority': 'whitelist',
'language': 'cn'
}
def load_config(config_file='config/config.ini'):
try:
config = ConfigParser()
config.read(config_file, encoding='utf-8')
if not config.has_section('Server'):
config.add_section('Server')
for key, value in DEFAULT_CONFIG.items():
config.set('Server', key, str(value))
with open(config_file, 'w', encoding='utf-8') as f:
config.write(f)
result = dict(config.items('Server'))
if config.has_section('Users'):
result['Users'] = dict(config.items('Users'))
return result
except Exception as e:
logging.error(f"Error loading config: {e}")
return DEFAULT_CONFIG.copy()
def load_ip_list(file_path):
try:
config_path = os.path.join('config', os.path.basename(file_path))
if os.path.exists(config_path):
with open(config_path, 'r', encoding='utf-8') as f:
return set(line.strip() for line in f if line.strip())
except Exception as e:
logging.error(f"Error loading IP list: {e}")
return set()
_proxy_check_cache = {}
_proxy_check_ttl = 10
def parse_proxy(proxy):
try:
protocol = proxy.split('://')[0]
remaining = proxy.split('://')[1]
if '@' in remaining:
auth, address = remaining.split('@')
host, port = address.split(':')
return protocol, auth, host, int(port)
else:
host, port = remaining.split(':')
return protocol, None, host, int(port)
except Exception:
return None, None, None, None
async def check_http_proxy(proxy, test_url=None):
if test_url is None:
test_url = 'https://www.baidu.com'
protocol, auth, host, port = parse_proxy(proxy)
proxies = {}
if auth:
proxies['http://'] = f'{protocol}://{auth}@{host}:{port}'
proxies['https://'] = f'{protocol}://{auth}@{host}:{port}'
else:
proxies['http://'] = f'{protocol}://{host}:{port}'
proxies['https://'] = f'{protocol}://{host}:{port}'
try:
async with httpx.AsyncClient(proxies=proxies, timeout=10, verify=False) as client:
try:
response = await client.get(test_url)
return response.status_code == 200
except:
if test_url.startswith('https://'):
http_url = 'http://' + test_url[8:]
response = await client.get(http_url)
return response.status_code == 200
return False
except:
return False
async def check_socks_proxy(proxy, test_url=None):
if test_url is None:
test_url = 'https://www.baidu.com'
protocol, auth, host, port = parse_proxy(proxy)
if not all([host, port]):
return False
try:
reader, writer = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=5)
if auth:
writer.write(b'\x05\x02\x00\x02')
else:
writer.write(b'\x05\x01\x00')
await writer.drain()
auth_method = await asyncio.wait_for(reader.readexactly(2), timeout=5)
if auth_method[0] != 0x05:
return False
if auth_method[1] == 0x02 and auth:
username, password = auth.split(':')
auth_packet = bytes([0x01, len(username)]) + username.encode() + bytes([len(password)]) + password.encode()
writer.write(auth_packet)
await writer.drain()
auth_response = await asyncio.wait_for(reader.readexactly(2), timeout=5)
if auth_response[1] != 0x00:
return False
from urllib.parse import urlparse
domain = urlparse(test_url).netloc if '://' in test_url else test_url
domain = domain.encode()
writer.write(b'\x05\x01\x00\x03' + bytes([len(domain)]) + domain + b'\x00\x50')
await writer.drain()
response = await asyncio.wait_for(reader.readexactly(10), timeout=5)
writer.close()
try:
await writer.wait_closed()
except:
pass
return response[1] == 0x00
except Exception:
return False
async def check_proxy(proxy, test_url=None):
current_time = time.time()
cache_key = f"{proxy}:{test_url}"
if cache_key in _proxy_check_cache:
cache_time, is_valid = _proxy_check_cache[cache_key]
if current_time - cache_time < _proxy_check_ttl:
return is_valid
proxy_type = proxy.split('://')[0]
check_funcs = {
'http': check_http_proxy,
'https': check_http_proxy,
'socks5': check_socks_proxy
}
if proxy_type not in check_funcs:
return False
try:
test_url = test_url or 'https://www.baidu.com'
is_valid = await check_funcs[proxy_type](proxy, test_url)
_proxy_check_cache[cache_key] = (current_time, is_valid)
return is_valid
except Exception:
_proxy_check_cache[cache_key] = (current_time, False)
return False
async def check_proxies(proxies, test_url=None):
valid_proxies = []
for proxy in proxies:
if await check_proxy(proxy, test_url):
valid_proxies.append(proxy)
return valid_proxies
async def check_for_updates(language='cn'):
try:
async with httpx.AsyncClient() as client:
response = await asyncio.wait_for(client.get("https://y.shironekosan.cn/1.html"), timeout=10)
response.raise_for_status()
content = response.text
match = re.search(r'<p>(ProxyCat-V\d+\.\d+\.\d+)</p>', content)
if match:
latest_version = match.group(1)
CURRENT_VERSION = "ProxyCat-V2.0.4"
if version.parse(latest_version.split('-V')[1]) > version.parse(CURRENT_VERSION.split('-V')[1]):
print(f"{Fore.YELLOW}{get_message('new_version_found', language)} 当前版本: {CURRENT_VERSION}, 最新版本: {latest_version}{Style.RESET_ALL}")
print(f"{Fore.YELLOW}{get_message('visit_quark', language)}{Style.RESET_ALL}")
print(f"{Fore.YELLOW}{get_message('visit_github', language)}{Style.RESET_ALL}")
print(f"{Fore.YELLOW}{get_message('visit_baidu', language)}{Style.RESET_ALL}")
else:
print(f"{Fore.GREEN}{get_message('latest_version', language)} ({CURRENT_VERSION}){Style.RESET_ALL}")
else:
print(f"{Fore.RED}{get_message('version_info_not_found', language)}{Style.RESET_ALL}")
except Exception as e:
print(f"{Fore.RED}{get_message('update_check_error', language, e)}{Style.RESET_ALL}")
================================================
FILE: modules/proxyserver.py
================================================
import asyncio, httpx, logging, re, socket, struct, time, base64, random, os
from modules.modules import get_message, load_ip_list
from asyncio import TimeoutError
from itertools import cycle
from config import getip
from configparser import ConfigParser
def load_proxies(file_path='ip.txt'):
with open(file_path, 'r') as file:
return [line.strip() for line in file if '://' in line]
def validate_proxy(proxy):
pattern = re.compile(r'^(?P<scheme>socks5|http|https)://(?:(?P<auth>[^@]+)@)?(?P<host>[^:]+):(?P<port>\d+)$')
match = pattern.match(proxy)
if not match:
return False
port = int(match.group('port'))
return 0 < port < 65536
class AsyncProxyServer:
def __init__(self, config):
self.config = config
self._init_config_values(config)
self._init_server_state()
self._init_connection_settings()
self.proxy_failure_lock = asyncio.Lock()
def _init_config_values(self, config):
self.port = int(config.get('port', '1080'))
self.mode = config.get('mode', 'cycle')
self.interval = int(config.get('interval', '300'))
self.language = config.get('language', 'cn')
self.use_getip = config.get('use_getip', 'False').lower() == 'true'
self.check_proxies = config.get('check_proxies', 'True').lower() == 'true'
self.username = config.get('username', '')
self.password = config.get('password', '')
self.proxy_username = config.get('proxy_username', '')
self.proxy_password = config.get('proxy_password', '')
self.users = {}
if 'Users' in config:
self.users = dict(config['Users'].items())
self.auth_required = bool(self.users)
self.proxy_file = os.path.join('config', os.path.basename(config.get('proxy_file', 'ip.txt')))
self.whitelist_file = os.path.join('config', os.path.basename(config.get('whitelist_file', 'whitelist.txt')))
self.blacklist_file = os.path.join('config', os.path.basename(config.get('blacklist_file', 'blacklist.txt')))
self.ip_auth_priority = config.get('ip_auth_priority', 'whitelist')
self.test_url = config.get('test_url', 'https://www.baidu.com')
self.whitelist = load_ip_list(self.whitelist_file)
self.blacklist = load_ip_list(self.blacklist_file)
if self.use_getip:
self.getip_url = config.get('getip_url', '')
self.switching_proxy = False
self.last_switch_attempt = 0
self.switch_cooldown = 5
self.proxy_check_cache = {}
self.last_check_time = {}
self.proxy_check_ttl = 60
self.check_cooldown = 10
self.connected_clients = set()
self.last_proxy_failure_time = 0
self.proxy_failure_cooldown = 3
def _init_server_state(self):
self.running = False
self.stop_server = False
self.server_instance = None
self.tasks = set()
self.last_switch_time = time.time()
self.proxy_cycle = None
self.current_proxy = None
self.proxies = []
self.known_clients = set()
if not self.use_getip:
self.proxies = self._load_file_proxies()
if self.proxies:
self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle)
def _init_connection_settings(self):
self.buffer_size = 8192
self.connection_timeout = 30
self.read_timeout = 60
self.max_concurrent_requests = 1000
self.request_semaphore = asyncio.Semaphore(self.max_concurrent_requests)
self.connection_pool = {}
self.max_pool_size = 500
self.client_pool = {}
self.client_pool_lock = asyncio.Lock()
self.proxy_pool = {}
self.active_connections = set()
def _update_config_values(self, new_config):
self._init_config_values(new_config)
self.last_switch_time = time.time()
self.last_switch_attempt = 0
def _handle_mode_change(self):
self.last_switch_attempt = 0
if self.use_getip:
self.proxies = []
self.proxy_cycle = None
self.current_proxy = None
logging.info(get_message('api_mode_notice', self.language))
else:
logging.info(f"切换到{'负载均衡' if self.mode == 'loadbalance' else '循环模式'}模式,从 {self.proxy_file} 加载代理列表")
self.proxies = self._load_file_proxies()
logging.info(f"加载到 {len(self.proxies)} 个代理")
if self.proxies:
self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle)
logging.info(f"当前使用代理: {self.current_proxy}")
if self.check_proxies and self.mode != 'loadbalance':
try:
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.create_task(self._check_proxies_wrapper())
else:
loop.run_until_complete(self._check_proxies())
except Exception as e:
logging.error(f"检查代理时出错: {str(e)}")
else:
logging.error(f"从文件 {self.proxy_file} 加载代理失败,请检查文件是否存在且包含有效代理")
async def _check_proxies_wrapper(self):
"""包装 _check_proxies 方法,用于在已运行的事件循环中调用"""
await self._check_proxies()
def _reload_proxies(self):
self.last_switch_attempt = 0
logging.info(f"重新加载代理列表文件 {self.proxy_file}")
self.proxies = self._load_file_proxies()
logging.info(f"加载到 {len(self.proxies)} 个代理")
if self.proxies:
self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle)
logging.info(f"当前使用代理: {self.current_proxy}")
if self.check_proxies:
try:
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.create_task(self._check_proxies_wrapper())
else:
loop.run_until_complete(self._check_proxies())
except Exception as e:
logging.error(f"检查代理时出错: {str(e)}")
else:
logging.error(f"从文件 {self.proxy_file} 加载代理失败,请检查文件是否存在且包含有效代理")
async def _check_proxies(self):
from modules.modules import check_proxies
valid_proxies = await check_proxies(self.proxies, test_url=self.test_url)
if valid_proxies:
self.proxies = valid_proxies
self.proxy_cycle = cycle(valid_proxies)
self.current_proxy = next(self.proxy_cycle)
def _load_file_proxies(self):
try:
proxy_file = os.path.join('config', os.path.basename(self.proxy_file))
if os.path.exists(proxy_file):
with open(proxy_file, 'r', encoding='utf-8') as f:
proxies = [line.strip() for line in f if line.strip()]
return proxies
else:
logging.error(get_message('proxy_file_not_found', self.language, proxy_file))
return []
except Exception as e:
logging.error(get_message('load_proxy_file_error', self.language, str(e)))
return []
async def start(self):
if not self.running:
self.stop_server = False
self.running = True
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 1024)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024 * 1024)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.bind(('0.0.0.0', self.port))
loop = asyncio.get_event_loop()
if hasattr(loop, 'set_default_executor'):
import concurrent.futures
executor = concurrent.futures.ThreadPoolExecutor(max_workers=max(32, os.cpu_count() * 4))
loop.set_default_executor(executor)
server = await asyncio.start_server(
self.handle_client,
sock=sock,
backlog=2048,
limit=32768,
)
self.server_instance = server
logging.info(get_message('server_running', self.language, '0.0.0.0', self.port))
self.tasks.add(asyncio.create_task(self.cleanup_clients()))
self.tasks.add(asyncio.create_task(self._cleanup_pool()))
self.tasks.add(asyncio.create_task(self.cleanup_disconnected_ips()))
if hasattr(os, 'sched_setaffinity'):
try:
os.sched_setaffinity(0, range(os.cpu_count()))
except:
pass
async with server:
await server.serve_forever()
except Exception as e:
if not self.stop_server:
logging.error(get_message('server_start_error', self.language, str(e)))
finally:
self.running = False
self.server_instance = None
async def stop(self):
if self.running:
self.stop_server = True
if self.server_instance:
self.server_instance.close()
await self.server_instance.wait_closed()
self.server_instance = None
for task in self.tasks:
task.cancel()
if self.tasks:
await asyncio.gather(*self.tasks, return_exceptions=True)
self.tasks.clear()
self.running = False
logging.info(get_message('server_shutting_down', self.language))
async def get_next_proxy(self):
try:
current_time = time.time()
if self.mode == 'loadbalance' and self.proxies:
if not self.switching_proxy:
try:
self.switching_proxy = True
self.last_switch_attempt = current_time
if not self.use_getip:
if not self.proxy_cycle:
self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle)
logging.info(f"负载均衡模式选择代理: {self.current_proxy}")
else:
await self.get_proxy()
finally:
self.switching_proxy = False
return self.current_proxy
if self.switching_proxy or (current_time - self.last_switch_attempt < self.switch_cooldown):
return self.current_proxy
if self.interval > 0 and current_time - self.last_switch_time >= self.interval or \
(self.use_getip and not self.current_proxy):
try:
self.switching_proxy = True
self.last_switch_attempt = current_time
old_proxy = self.current_proxy
await self.get_proxy()
finally:
self.switching_proxy = False
return self.current_proxy
except Exception as e:
logging.error(get_message('proxy_switch_error', self.language, str(e)))
self.switching_proxy = False
return self.current_proxy
async def _load_getip_proxy(self):
valid_proxies = []
for _ in range(4):
new_ip = getip.newip()
if validate_proxy(new_ip):
valid_proxies.append(new_ip)
break
else:
logging.error(get_message('multiple_proxy_fail', self.language))
exit(1)
return valid_proxies[0]
def time_until_next_switch(self):
return float('inf') if self.mode == 'loadbalance' else max(0, self.interval - (time.time() - self.last_switch_time))
def check_ip_auth(self, ip):
try:
if not self.whitelist and not self.blacklist:
return True
if self.ip_auth_priority == 'whitelist':
if self.whitelist:
if ip in self.whitelist:
return True
return False
if self.blacklist:
return ip not in self.blacklist
return True
else:
if ip in self.blacklist:
return False
if self.whitelist:
return ip in self.whitelist
return True
except Exception as e:
logging.error(get_message('whitelist_error', self.language, str(e)))
return False
def _authenticate(self, headers):
if not self.auth_required:
return True
auth_header = headers.get('proxy-authorization', '')
if not auth_header:
return False
try:
scheme, credentials = auth_header.split()
if scheme.lower() != 'basic':
return False
decoded = base64.b64decode(credentials).decode()
username, password = decoded.split(':')
if username in self.users and self.users[username] == password:
return username, password
except Exception:
pass
return False
async def _close_connection(self, writer):
try:
if writer and not writer.is_closing():
writer.write_eof()
await writer.drain()
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
except Exception:
pass
async def handle_client(self, reader, writer):
task = asyncio.current_task()
self.tasks.add(task)
peername = writer.get_extra_info('peername')
if peername:
self.active_connections.add(peername)
try:
peername = writer.get_extra_info('peername')
if peername:
client_ip = peername[0]
if not self.check_ip_auth(client_ip):
logging.warning(get_message('unauthorized_ip', self.language, client_ip))
writer.write(b'HTTP/1.1 403 Forbidden\r\n\r\n')
await writer.drain()
return
first_byte = await reader.read(1)
if not first_byte:
return
if first_byte == b'\x05':
await self.handle_socks5_connection(reader, writer)
else:
await self._handle_client_impl(reader, writer, first_byte)
except Exception as e:
logging.error(get_message('client_handle_error', self.language, e))
finally:
if peername:
self.active_connections.discard(peername)
await self._close_connection(writer)
self.tasks.remove(task)
async def _pipe(self, reader, writer):
try:
while True:
try:
data = await reader.read(self.buffer_size)
if not data:
break
try:
writer.write(data)
await writer.drain()
except (ConnectionError, ConnectionResetError):
await self.handle_proxy_failure()
break
except (ConnectionError, ConnectionResetError):
await self.handle_proxy_failure()
break
except asyncio.CancelledError:
pass
except Exception as e:
await self.handle_proxy_failure()
pass
finally:
await self._close_connection(writer)
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 _create_client(self, proxy):
proxy_type, proxy_addr = proxy.split('://')
proxy_auth = None
if '@' in proxy_addr:
auth, proxy_addr = proxy_addr.split('@')
proxy_auth = auth
if proxy_auth:
proxy_url = f"{proxy_type}://{proxy_auth}@{proxy_addr}"
else:
proxy_url = f"{proxy_type}://{proxy_addr}"
import logging as httpx_logging
httpx_logging.getLogger("httpx").setLevel(logging.WARNING)
httpx_logging.getLogger("hpack").setLevel(logging.WARNING)
httpx_logging.getLogger("h2").setLevel(logging.WARNING)
return httpx.AsyncClient(
proxies={"all://": proxy_url},
limits=httpx.Limits(
max_keepalive_connections=100,
max_connections=1000,
keepalive_expiry=30
),
timeout=30.0,
http2=True,
verify=False,
follow_redirects=True
)
async def _cleanup_connections(self):
current_time = time.time()
expired_keys = [
key for key, client in self.connection_pool.items()
if current_time - client._last_used > 30
]
for key in expired_keys:
client = self.connection_pool.pop(key)
await client.aclose()
async def handle_socks5_connection(self, reader, writer):
try:
nmethods = ord(await reader.readexactly(1))
await reader.readexactly(nmethods)
writer.write(b'\x05\x02' if self.auth_required else b'\x05\x00')
await writer.drain()
if self.auth_required:
auth_version = await reader.readexactly(1)
if auth_version != b'\x01':
writer.close()
return
ulen = ord(await reader.readexactly(1))
username = await reader.readexactly(ulen)
plen = ord(await reader.readexactly(1))
password = await reader.readexactly(plen)
username = username.decode()
password = password.decode()
if username in self.users and self.users[username] == password:
peername = writer.get_extra_info('peername')
if peername:
client_ip = peername[0]
client_key = (client_ip, username)
if client_key not in self.known_clients:
self.known_clients.add(client_key)
logging.info(get_message('new_client_connect', self.language, client_ip, f"{username}:{password}"))
else:
writer.write(b'\x01\x01')
await writer.drain()
writer.close()
return
writer.write(b'\x01\x00')
await writer.drain()
version, cmd, _, atyp = struct.unpack('!BBBB', await reader.readexactly(4))
if cmd != 1:
writer.write(b'\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00')
await writer.drain()
writer.close()
return
if atyp == 1:
dst_addr = socket.inet_ntoa(await reader.readexactly(4))
elif atyp == 3:
addr_len = ord(await reader.readexactly(1))
dst_addr = (await reader.readexactly(addr_len)).decode()
elif atyp == 4:
dst_addr = socket.inet_ntop(socket.AF_INET6, await reader.readexactly(16))
else:
writer.write(b'\x05\x08\x00\x01\x00\x00\x00\x00\x00\x00')
await writer.drain()
writer.close()
return
dst_port = struct.unpack('!H', await reader.readexactly(2))[0]
max_retries = 1
retry_count = 0
last_error = None
while retry_count < max_retries:
try:
proxy = await self.get_next_proxy()
if not proxy:
raise Exception("No proxy available")
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)
remote_reader, remote_writer = await asyncio.wait_for(
asyncio.open_connection(proxy_host, proxy_port),
timeout=10
)
if proxy_type == 'socks5':
await self._initiate_socks5(remote_reader, remote_writer, dst_addr, dst_port)
elif proxy_type in ['http', 'https']:
await self._initiate_http(remote_reader, remote_writer, dst_addr, dst_port, proxy_auth)
writer.write(b'\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00')
await writer.drain()
await asyncio.gather(
self._pipe(reader, remote_writer),
self._pipe(remote_reader, writer)
)
return
except (asyncio.TimeoutError, ConnectionRefusedError, ConnectionResetError) as e:
last_error = e
logging.warning(get_message('request_retry', self.language, max_retries - retry_count - 1))
await self.handle_proxy_failure()
retry_count += 1
if retry_count < max_retries:
await asyncio.sleep(1)
continue
except Exception as e:
last_error = e
logging.error(get_message('socks5_connection_error', self.language, str(e)))
await self.handle_proxy_failure()
retry_count += 1
if retry_count < max_retries:
await asyncio.sleep(1)
continue
#if last_error:
#logging.error(get_message('all_retries_failed', self.language, str(last_error)))
writer.write(b'\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00')
await writer.drain()
except Exception as e:
logging.error(get_message('socks5_connection_error', self.language, str(e)))
writer.write(b'\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00')
await writer.drain()
async def _initiate_socks5(self, remote_reader, remote_writer, dst_addr, dst_port):
try:
auth = None
proxy_type, proxy_addr = self.current_proxy.split('://')
if '@' in proxy_addr:
auth, _ = proxy_addr.split('@')
if auth:
remote_writer.write(b'\x05\x02\x00\x02')
else:
remote_writer.write(b'\x05\x01\x00')
await remote_writer.drain()
try:
auth_method = await asyncio.wait_for(
remote_reader.readexactly(2),
timeout=10
)
if auth_method[0] != 0x05:
raise Exception("Invalid SOCKS5 proxy response")
if auth_method[1] == 0x02 and auth:
username, password = auth.split(':')
auth_packet = bytes([0x01, len(username)]) + username.encode() + bytes([len(password)]) + password.encode()
remote_writer.write(auth_packet)
await remote_writer.drain()
auth_response = await asyncio.wait_for(
remote_reader.readexactly(2),
timeout=10
)
if auth_response[1] != 0x00:
raise Exception("Authentication failed")
if isinstance(dst_addr, str):
remote_writer.write(b'\x05\x01\x00\x03' + len(dst_addr).to_bytes(1, 'big') +
dst_addr.encode() + dst_port.to_bytes(2, 'big'))
else:
remote_writer.write(b'\x05\x01\x00\x01' + socket.inet_aton(dst_addr) +
dst_port.to_bytes(2, 'big'))
await remote_writer.drain()
response = await asyncio.wait_for(
remote_reader.readexactly(4),
timeout=10
)
if response[1] != 0x00:
error_codes = {
0x01: "General failure",
0x02: "Connection not allowed",
0x03: "Network unreachable",
0x04: "Host unreachable",
0x05: "Connection refused",
0x06: "TTL expired",
0x07: "Command not supported",
0x08: "Address type not supported"
}
error_msg = error_codes.get(response[1], f"Unknown error code {response[1]}")
raise Exception(f"Connection failed: {error_msg}")
if response[3] == 0x01:
await asyncio.wait_for(
remote_reader.readexactly(6),
timeout=10
)
elif response[3] == 0x03:
domain_len = (await asyncio.wait_for(
remote_reader.readexactly(1),
timeout=10
))[0]
await asyncio.wait_for(
remote_reader.readexactly(domain_len + 2),
timeout=10
)
elif response[3] == 0x04:
await asyncio.wait_for(
remote_reader.readexactly(18),
timeout=10
)
else:
raise Exception(f"Unsupported address type: {response[3]}")
except asyncio.TimeoutError:
raise Exception("SOCKS5 proxy response timeout")
except Exception as e:
raise Exception(f"SOCKS5 protocol error: {str(e)}")
except Exception as e:
if isinstance(e, asyncio.TimeoutError):
raise Exception("SOCKS5 connection timeout")
elif "Connection refused" in str(e):
raise Exception("SOCKS5 connection refused")
else:
raise Exception(f"SOCKS5 initialization failed: {str(e)}")
async def _initiate_http(self, remote_reader, remote_writer, dst_addr, dst_port, proxy_auth):
connect_request = f'CONNECT {dst_addr}:{dst_port} HTTP/1.1\r\nHost: {dst_addr}:{dst_port}\r\n'
if proxy_auth:
connect_request += f'Proxy-Authorization: Basic {base64.b64encode(proxy_auth.encode()).decode()}\r\n'
connect_request += '\r\n'
remote_writer.write(connect_request.encode())
await remote_writer.drain()
while True:
line = await remote_reader.readline()
if line == b'\r\n':
break
async def _handle_client_impl(self, reader, writer, first_byte):
try:
peername = writer.get_extra_info('peername')
client_info = f"{peername[0]}:{peername[1]}" if peername else "未知客户端"
if peername:
client_ip = peername[0]
if not self.check_ip_auth(client_ip):
logging.warning(get_message('unauthorized_ip', self.language, client_ip))
writer.write(b'HTTP/1.1 403 Forbidden\r\n\r\n')
await writer.drain()
return
request_line = first_byte + await reader.readline()
if not request_line:
return
try:
method, path, _ = request_line.decode('utf-8', errors='ignore').split()
except (ValueError, UnicodeDecodeError) as e:
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:
continue
if self.auth_required:
auth_result = self._authenticate(headers)
if not auth_result:
writer.write(b'HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="Proxy"\r\n\r\n')
await writer.drain()
return
elif isinstance(auth_result, tuple):
username, password = auth_result
peername = writer.get_extra_info('peername')
if peername:
client_ip = peername[0]
client_key = (client_ip, username)
if client_key not in self.known_clients:
self.known_clients.add(client_key)
logging.info(get_message('new_client_connect', self.language, client_ip, f"{username}:{password}"))
if method == 'CONNECT':
await self._handle_connect(path, reader, writer)
else:
await self._handle_request(method, path, headers, reader, writer)
except (ConnectionError, ConnectionResetError, ConnectionAbortedError):
return
except asyncio.CancelledError:
return
except Exception as e:
if not isinstance(e, (ConnectionError, ConnectionResetError, ConnectionAbortedError,
asyncio.CancelledError, asyncio.TimeoutError)):
logging.error(get_message('client_request_error', self.language, str(e)))
async def _handle_connect(self, path, reader, writer):
try:
host, port = path.split(':')
port = int(port)
except ValueError:
writer.write(b'HTTP/1.1 400 Bad Request\r\n\r\n')
await writer.drain()
return
max_retries = 1
retry_count = 0
last_error = None
while retry_count < max_retries:
try:
proxy = await self.get_next_proxy()
if not proxy:
writer.write(b'HTTP/1.1 503 Service Unavailable\r\n\r\n')
await writer.drain()
return
try:
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)
remote_reader, remote_writer = await asyncio.wait_for(
asyncio.open_connection(proxy_host, proxy_port),
timeout=10
)
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'):
await self.handle_proxy_failure()
last_error = f"Bad Gateway: {response.decode('utf-8', errors='ignore')}"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
raise Exception("Bad Gateway")
while (await remote_reader.readline()) != b'\r\n':
pass
elif proxy_type == 'socks5':
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:
await self.handle_proxy_failure()
last_error = "SOCKS5 connection failed"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
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)
)
return
except asyncio.TimeoutError:
await self.handle_proxy_failure()
last_error = "Connection Timeout"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
logging.error(get_message('connect_timeout', self.language))
writer.write(b'HTTP/1.1 504 Gateway Timeout\r\n\r\n')
await writer.drain()
return
except Exception as e:
await self.handle_proxy_failure()
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
return
except Exception as e:
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
return
#if last_error:
#logging.error(get_message('all_retries_failed', self.language, last_error))
async def _handle_request(self, method, path, headers, reader, writer):
async with self.request_semaphore:
max_retries = 1
retry_count = 0
last_error = None
while retry_count < max_retries:
try:
proxy = await self.get_next_proxy()
if not proxy:
writer.write(b'HTTP/1.1 503 Service Unavailable\r\n\r\n')
await writer.drain()
return
try:
client = await self._get_client(proxy)
proxy_headers = headers.copy()
proxy_type, proxy_addr = proxy.split('://')
if '@' in proxy_addr:
auth, _ = proxy_addr.split('@')
auth_header = f'Basic {base64.b64encode(auth.encode()).decode()}'
proxy_headers['Proxy-Authorization'] = auth_header
try:
async with client.stream(
method,
path,
headers=proxy_headers,
content=reader,
timeout=30.0
) as response:
writer.write(f'HTTP/1.1 {response.status_code} {response.reason_phrase}\r\n'.encode())
for header_name, header_value in response.headers.items():
if header_name.lower() not in ('transfer-encoding', 'connection'):
writer.write(f'{header_name}: {header_value}\r\n'.encode())
writer.write(b'\r\n')
try:
async for chunk in response.aiter_bytes(chunk_size=self.buffer_size):
if not chunk:
break
try:
writer.write(chunk)
if len(chunk) >= self.buffer_size:
await writer.drain()
except (ConnectionError, ConnectionResetError, ConnectionAbortedError):
return
except Exception:
break
await writer.drain()
except (ConnectionError, ConnectionResetError, ConnectionAbortedError):
return
except Exception:
pass
return
except httpx.RequestError:
await self.handle_proxy_failure()
last_error = "Request Error"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
except Exception as e:
if isinstance(e, (ConnectionError, ConnectionResetError, ConnectionAbortedError)):
await self.handle_proxy_failure()
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
return
except httpx.HTTPError:
await self.handle_proxy_failure()
last_error = "HTTP Error"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
except Exception as e:
if isinstance(e, (ConnectionError, ConnectionResetError, ConnectionAbortedError)):
await self.handle_proxy_failure()
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
return
except Exception as e:
if isinstance(e, (ConnectionError, ConnectionResetError, ConnectionAbortedError)):
await self.handle_proxy_failure()
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
if not isinstance(e, (asyncio.CancelledError,)):
logging.error(f"请求处理错误: {str(e)}")
try:
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
except:
pass
return
#if last_error:
logging.error(get_message('all_retries_failed', self.language, last_error))
async def _get_client(self, proxy):
async with self.client_pool_lock:
current_time = time.time()
if proxy in self.client_pool:
client, last_used = self.client_pool[proxy]
if current_time - last_used < 30 and not client.is_closed:
self.client_pool[proxy] = (client, current_time)
return client
else:
await client.aclose()
del self.client_pool[proxy]
try:
client = await self._create_client(proxy)
if len(self.client_pool) >= self.max_pool_size:
oldest_proxy = min(self.client_pool, key=lambda x: self.client_pool[x][1])
old_client, _ = self.client_pool[oldest_proxy]
await old_client.aclose()
del self.client_pool[oldest_proxy]
self.client_pool[proxy] = (client, current_time)
return client
except Exception as e:
logging.error(f"创建客户端失败: {str(e)}")
raise
async def handle_proxy_failure(self):
if not self.check_proxies:
return
current_time = time.time()
if current_time - self.last_proxy_failure_time < self.proxy_failure_cooldown:
return
if self.switching_proxy:
return
try:
if not self.proxy_failure_lock.locked():
async with self.proxy_failure_lock:
if (current_time - self.last_proxy_failure_time < self.proxy_failure_cooldown or
self.switching_proxy):
return
self.last_proxy_failure_time = current_time
try:
is_valid = await self.check_current_proxy()
if not is_valid:
#logging.warning(get_message('proxy_failure', self.language, self.current_proxy))
await self.switch_proxy()
except Exception as e:
logging.error(get_message('proxy_check_error', self.language, str(e)))
except Exception as e:
logging.error(f"代理失败处理出错: {str(e)}")
async def switch_proxy(self):
try:
current_time = time.time()
if current_time - self.last_switch_attempt < self.switch_cooldown:
return False
if self.switching_proxy:
return False
self.switching_proxy = True
self.last_switch_attempt = current_time
old_proxy = self.current_proxy
temp_current_proxy = self.current_proxy
await self.get_proxy()
if temp_current_proxy != self.current_proxy:
self._log_proxy_switch(old_proxy, self.current_proxy)
self.last_proxy_failure_time = current_time
return True
return False
except Exception as e:
logging.error(get_message('proxy_switch_error', self.language, str(e)))
return False
finally:
self.switching_proxy = False
async def check_current_proxy(self):
try:
proxy = self.current_proxy
if not proxy:
return False
current_time = time.time()
if not self.check_proxies:
return True
if proxy in self.last_check_time:
if current_time - self.last_check_time[proxy] < self.check_cooldown:
return self.proxy_check_cache.get(proxy, (current_time, True))[1]
if proxy in self.proxy_check_cache:
cache_time, is_valid = self.proxy_check_cache[proxy]
if current_time - cache_time < self.proxy_check_ttl:
return is_valid
self.last_check_time[proxy] = current_time
test_url = self.config.get('test_url', 'https://www.baidu.com')
try:
from modules.modules import check_proxy
is_valid = await check_proxy(proxy, test_url)
logging.warning(f"代理检查结果: {proxy} - {'有效' if is_valid else '无效'}")
except Exception as e:
logging.error(f"代理检测错误: {proxy} - {str(e)}")
is_valid = False
self.proxy_check_cache[proxy] = (current_time, is_valid)
return is_valid
except Exception as e:
logging.error(f"代理检测异常: {str(e)}")
if 'proxy' in locals():
self.proxy_check_cache[proxy] = (current_time, False)
return False
def _clean_proxy_cache(self):
current_time = time.time()
self.proxy_check_cache = {
proxy: (cache_time, is_valid)
for proxy, (cache_time, is_valid) in self.proxy_check_cache.items()
if current_time - cache_time < self.proxy_check_ttl
}
self.last_check_time = {
proxy: check_time
for proxy, check_time in self.last_check_time.items()
if current_time - check_time < self.proxy_check_ttl
}
def initialize_proxies(self):
if hasattr(self, 'proxies') and self.proxies:
self.proxy_cycle = cycle(self.proxies)
return
if self.use_getip:
logging.info("API模式,将在请求时动态获取代理")
return
try:
logging.info(f"从文件 {self.proxy_file} 加载代理列表")
self.proxies = self._load_file_proxies()
logging.info(f"加载到 {len(self.proxies)} 个代理")
if self.proxies:
self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle)
logging.info(f"初始代理: {self.current_proxy}")
except Exception as e:
logging.error(f"初始化代理列表失败: {str(e)}")
async def cleanup_disconnected_ips(self):
while True:
try:
active_ips = {addr[0] for addr in self.active_connections}
self.connected_clients = active_ips
except Exception as e:
logging.error(get_message('cleanup_error', self.language, str(e)))
await asyncio.sleep(30)
def is_docker():
return os.path.exists('/.dockerenv')
async def get_proxy_status(self):
if self.mode == 'loadbalance':
return f"{get_message('current_proxy', self.language)}: {self.current_proxy}"
else:
time_left = self.time_until_next_switch()
if time_left == float('inf'):
return f"{get_message('current_proxy', self.language)}: {self.current_proxy}"
else:
return f"{get_message('current_proxy', self.language)}: {self.current_proxy} | {get_message('next_switch', self.language)}: {time_left:.1f}{get_message('seconds', self.language)}"
async def _get_proxy_connection(self, proxy):
if proxy in self.proxy_pool:
conn = self.proxy_pool[proxy]
if not conn.is_closed:
conn._last_used = time.time()
return conn
proxy_type, proxy_addr = proxy.split('://')
if '@' in proxy_addr:
auth, addr = proxy_addr.split('@')
username, password = auth.split(':')
else:
username = self.username
password = self.password
addr = proxy_addr
host, port = addr.split(':')
port = int(port)
if proxy_type in ('socks5', 'socks4'):
conn = await self._create_socks_connection(
host, port, username, password,
proxy_type == 'socks5'
)
else:
conn = await self._create_http_connection(
host, port, username, password
)
if len(self.proxy_pool) < self.max_pool_size:
conn._last_used = time.time()
self.proxy_pool[proxy] = conn
return conn
async def _create_socks_connection(self, host, port, username, password, is_socks5):
reader, writer = await asyncio.open_connection(
host, port,
limit=self.buffer_size
)
if is_socks5:
writer.write(b'\x05\x02\x00\x02' if username else b'\x05\x01\x00')
await writer.drain()
version, method = await reader.readexactly(2)
if version != 5:
raise Exception('Invalid SOCKS version')
if method == 2 and username:
auth = bytes([1, len(username)]) + username.encode() + \
bytes([len(password)]) + password.encode()
writer.write(auth)
await writer.drain()
auth_version, status = await reader.readexactly(2)
if status != 0:
raise Exception('Authentication failed')
return reader, writer
async def _create_http_connection(self, host, port, username, password):
reader, writer = await asyncio.open_connection(
host, port,
limit=self.buffer_size
)
if username:
auth = base64.b64encode(f'{username}:{password}'.encode()).decode()
writer.write(f'Proxy-Authorization: Basic {auth}\r\n'.encode())
await writer.drain()
return reader, writer
async def _cleanup_pool(self):
while True:
try:
def is_expired(conn):
return hasattr(conn, 'is_closed') and conn.is_closed
to_remove = []
for proxy, conn in list(self.proxy_pool.items()):
if is_expired(conn):
to_remove.append(proxy)
for proxy in to_remove:
if proxy in self.proxy_pool:
del self.proxy_pool[proxy]
except Exception as e:
logging.error(f'连接池清理错误: {e}')
await asyncio.sleep(60)
def _log_proxy_switch(self, old_proxy, new_proxy):
if old_proxy != new_proxy:
old_proxy = old_proxy if old_proxy else get_message('no_proxy', self.language)
new_proxy = new_proxy if new_proxy else get_message('no_proxy', self.language)
current_time = time.time()
if not hasattr(self, '_last_log_time') or \
not hasattr(self, '_last_log_content') or \
current_time - self._last_log_time > 1 or \
self._last_log_content != f"{old_proxy} -> {new_proxy}":
logging.info(get_message('proxy_switch', self.language, old_proxy, new_proxy))
self._last_log_time = current_time
self._last_log_content = f"{old_proxy} -> {new_proxy}"
async def _validate_proxy(self, proxy):
if not proxy:
return False
if not self.check_proxies:
return True
try:
if not validate_proxy(proxy):
logging.warning(get_message('proxy_invalid', self.language, proxy))
return False
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:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(proxy_host, proxy_port),
timeout=5
)
writer.close()
try:
await writer.wait_closed()
except:
pass
return True
except:
return False
except Exception as e:
logging.error(get_message('proxy_check_failed', self.language, proxy, str(e)))
return False
async def get_proxy(self):
try:
old_proxy = self.current_proxy
temp_current_proxy = self.current_proxy
if not self.use_getip and self.proxies:
if not self.proxy_cycle:
self.proxy_cycle = cycle(self.proxies)
for _ in range(3):
new_proxy = next(self.proxy_cycle)
if await self._validate_proxy(new_proxy):
self.current_proxy = new_proxy
self.last_switch_time = time.time()
if temp_current_proxy != self.current_proxy:
self._log_proxy_switch(old_proxy, self.current_proxy)
return self.current_proxy
logging.error(get_message('no_valid_proxies', self.language))
return self.current_proxy
if self.use_getip:
try:
new_proxy = await self._load_getip_proxy()
if new_proxy and await self._validate_proxy(new_proxy):
self.current_proxy = new_proxy
self.last_switch_time = time.time()
if temp_current_proxy != self.current_proxy:
self._log_proxy_switch(old_proxy, self.current_proxy)
return self.current_proxy
else:
logging.error(get_message('proxy_get_failed', self.language))
except Exception as e:
logging.error(get_message('proxy_get_error', self.language, str(e)))
return self.current_proxy
except Exception as e:
logging.error(get_message('proxy_get_error', self.language, str(e)))
return self.current_proxy
async def cleanup_clients(self):
while True:
try:
async with self.client_pool_lock:
current_time = time.time()
expired_proxies = [
proxy for proxy, (_, last_used) in self.client_pool.items()
if current_time - last_used > 30
]
for proxy in expired_proxies:
client, _ = self.client_pool[proxy]
await client.aclose()
del self.client_pool[proxy]
except Exception as e:
logging.error(f"清理客户端池错误: {str(e)}")
await asyncio.sleep(30)
def get_active_connections(self):
active = []
for task in self.tasks:
if not task.done():
try:
coro = task.get_coro()
if coro.__qualname__.startswith('AsyncProxyServer.handle_client'):
writer = coro.cr_frame.f_locals.get('writer')
if writer:
peername = writer.get_extra_info('peername')
if peername:
active.append(peername)
except Exception:
continue
return active
================================================
FILE: requirements.txt
================================================
colorama==0.4.6
httpx==0.27.2
httpx[http2,socks]==0.27.2
packaging==24.1
Requests==2.32.3
tqdm>=4.65.0
flask>=2.0.1
werkzeug>=2.0.0
asyncio>=3.4.3
configparser>=5.0.0
================================================
FILE: web/static/css/animations.css
================================================
@keyframes slideIn {
from { transform: translateY(-20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeInUp 0.3s ease;
}
================================================
FILE: web/static/css/base.css
================================================
:root {
--primary-color: #3498db;
--secondary-color: #2ecc71;
--warning-color: #f1c40f;
--danger-color: #e74c3c;
--background-color: #f5f6fa;
--text-color: #2c3e50;
--border-color: #dcdde1;
--card-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
body {
background-color: var(--background-color);
color: var(--text-color);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.card {
background: #fff;
border-radius: 12px;
box-shadow: var(--card-shadow);
margin-bottom: 20px;
padding: 20px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
}
.section-title {
color: var(--text-color);
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--border-color);
}
================================================
FILE: web/static/css/buttons.css
================================================
.btn {
border: none;
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn i {
font-size: 1.1em;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-success {
background-color: var(--secondary-color);
color: white;
}
.btn-warning {
background-color: var(--warning-color);
color: white;
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn:hover {
transform: translateY(-1px);
filter: brightness(1.1);
}
.btn:active {
transform: translateY(1px);
}
================================================
FILE: web/static/css/dark-mode.css
================================================
@media (prefers-color-scheme: dark) {
:root {
--background-color: #1a1a1a;
--text-color: #ffffff;
--border-color: #2d2d2d;
--card-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
.card {
background: #2d2d2d;
}
.status-card {
background: linear-gradient(145deg, #2d2d2d, #252525);
}
.status-card .card-title {
color: #e0e0e0;
}
.status-card .card-value {
color: #3498db;
}
.status-card .card-footer {
color: #888;
}
.btn {
border: 1px solid rgba(255, 255, 255, 0.1);
}
input, select, textarea {
background-color: #2d2d2d;
border-color: #3d3d3d;
color: #ffffff;
}
input:focus, select:focus, textarea:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
.progress {
background: rgba(255, 255, 255, 0.1);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
}
.progress-bar {
background: linear-gradient(45deg, #64b5f6, #4a90e2);
}
.card:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}
}
================================================
FILE: web/static/css/forms.css
================================================
.form-control, .form-select {
border-radius: 8px;
border: 2px solid #e9ecef;
padding: 10px 15px;
transition: all 0.3s ease;
}
.form-control:focus, .form-select:focus {
border-color: #4a90e2;
box-shadow: 0 0 0 0.2rem rgba(74, 144, 226, 0.25);
}
.table {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
}
.table thead th {
background: #f8f9fa;
border-bottom: 2px solid #dee2e6;
color: #2c3e50;
font-weight: 600;
padding: 15px;
}
.table tbody td {
padding: 12px 15px;
vertical-align: middle;
border-bottom: 1px solid #eee;
}
@media (prefers-color-scheme: dark) {
.form-control, .form-select {
background-color: #2d2d2d;
color: #ffffff;
border-color: #3d3d3d;
}
.table {
background-color: #2d2d2d;
color: #ffffff;
}
.table thead th {
background-color: #3d3d3d;
color: #ffffff;
}
}
/* 浮动保存按钮样式 */
.floating-save-container {
position: fixed;
bottom: 30px;
right: 30px;
z-index: 1000;
}
.floating-save-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
background: linear-gradient(135deg, #4a90e2, #357abd);
}
.floating-save-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
.floating-save-btn:active {
transform: translateY(1px);
}
.floating-save-btn i {
font-size: 1.1em;
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.floating-save-btn {
background: linear-gradient(135deg, #2980b9, #2c3e50);
}
}
/* 移动端适配 */
@media (max-width: 768px) {
.floating-save-container {
bottom: 20px;
right: 20px;
}
.floating-save-btn {
padding: 10px 20px;
}
}
/* Toast 样式优化 */
.toast {
position: fixed;
top: 20px;
right: 20px;
min-width: 300px;
padding: 16px 20px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 12px;
color: white;
transform: translateX(120%);
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1100;
font-weight: 500;
}
.toast.show {
transform: translateX(0);
opacity: 1;
}
/* 优化不同类型的 Toast 样式 */
.toast-success {
background: linear-gradient(135deg, #00b09b, #96c93d);
border-left: 4px solid #96c93d;
}
.toast-error {
background: linear-gradient(135deg, #ff5f6d, #ffc371);
border-left: 4px solid #ff5f6d;
}
.toast-info {
background: linear-gradient(135deg, #2193b0, #6dd5ed);
border-left: 4px solid #2193b0;
}
.toast-warning {
background: linear-gradient(135deg, #f2994a, #f2c94c);
border-left: 4px solid #f2994a;
}
/* Toast 图标样式 */
.toast i {
font-size: 1.4em;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
/* Toast 堆叠效果 */
.toast + .toast {
margin-top: 16px;
}
/* Toast 文本样式 */
.toast span {
flex: 1;
font-size: 14px;
line-height: 1.4;
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.toast {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.toast-success {
background: linear-gradient(135deg, #004d40, #1b5e20);
border-left: 4px solid #00c853;
}
.toast-error {
background: linear-gradient(135deg, #b71c1c, #c62828);
border-left: 4px solid #ff1744;
}
.toast-info {
background: linear-gradient(135deg, #0d47a1, #1976d2);
border-left: 4px solid #2196f3;
}
.toast-warning {
background: linear-gradient(135deg, #e65100, #f57c00);
border-left: 4px solid #ffd600;
}
}
/* 移动端适配 */
@media (max-width: 768px) {
.toast {
min-width: auto;
width: calc(100% - 32px);
margin: 0 16px;
padding: 12px 16px;
}
.toast + .toast {
margin-top: 12px;
}
}
================================================
FILE: web/static/css/logs.css
================================================
.log-container {
height: 500px;
position: relative;
margin: 1rem 0;
}
.log-INFO {
background-color: #e8f5e9;
color: #1b5e20;
border-left: 6px solid #2e7d32;
font-size: 17px;
font-weight: 600;
}
.log-WARNING {
background-color: #fff3e0;
color: #e65100;
border-left: 6px solid #f57c00;
font-size: 17px;
font-weight: 700;
}
.log-ERROR {
background-color: #ffebee;
color: #b71c1c;
border-left: 6px solid #c62828;
font-size: 18px;
font-weight: 800;
}
.log-CRITICAL {
background-color: #ffebee;
color: #7f0000;
border-left: 6px solid #b71c1c;
font-size: 18px;
font-weight: 900;
text-transform: uppercase;
}
.log-message {
font-weight: 600;
line-height: 1.5;
}
/* 搜索高亮样式 */
.log-highlight {
background-color: #fff176;
color: #000000;
padding: 2px 4px;
border-radius: 3px;
font-weight: bold;
margin: 0 2px;
}
/* 暗色模式下的高亮样式 */
@media (prefers-color-scheme: dark) {
.log-highlight {
background-color: #ffd600;
color: #000000;
}
.log-INFO {
background-color: rgba(232, 245, 233, 0.1);
color: #81c784;
border-left-color: #2e7d32;
}
.log-WARNING {
background-color: rgba(255, 243, 224, 0.1);
color: #ffb74d;
border-left-color: #f57c00;
}
.log-ERROR {
background-color: rgba(255, 235, 238, 0.1);
color: #e57373;
border-left-color: #c62828;
}
.log-CRITICAL {
background-color: rgba(255, 235, 238, 0.1);
color: #ef5350;
border-left-color: #b71c1c;
}
}
/* 日志条目样式 */
.log-entry {
padding: 8px 12px;
margin-bottom: 4px;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.log-entry:hover {
filter: brightness(0.95);
}
.log-time {
font-family: monospace;
opacity: 0.8;
}
.log-level {
font-weight: bold;
margin: 0 4px;
}
================================================
FILE: web/static/css/nav-tabs.css
================================================
.nav-tabs {
border-bottom: 2px solid #dee2e6;
margin-bottom: 20px;
}
.nav-tabs .nav-link {
border: none;
color: #6c757d;
padding: 10px 20px;
font-weight: 500;
transition: all 0.2s;
}
.nav-tabs .nav-link:hover {
border: none;
color: var(--primary-color);
}
.nav-tabs .nav-link.active {
border: none;
color: var(--primary-color);
position: relative;
}
.nav-tabs .nav-link.active:after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background-color: var(--primary-color);
}
================================================
FILE: web/static/css/progress.css
================================================
.progress-container {
margin-top: auto;
}
.progress {
height: 8px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.05);
overflow: hidden;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
.progress-bar {
background: linear-gradient(45deg, #4a90e2, #357abd);
box-shadow: 0 0 10px rgba(74, 144, 226, 0.5);
transition: width 0.3s ease;
position: relative;
overflow: hidden;
}
.progress-bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.3) 50%,
rgba(255, 255, 255, 0) 100%
);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
================================================
FILE: web/static/css/responsive.css
================================================
@media (max-width: 768px) {
.status-card {
margin-bottom: 20px;
}
.btn {
width: 100%;
margin-bottom: 10px;
}
.table-responsive {
margin-bottom: 20px;
}
.log-container {
height: 300px;
}
.log-search input {
font-size: 14px;
padding: 10px 40px;
}
}
.language-switch {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
background: white;
padding: 8px 16px;
border-radius: 20px;
box-shadow: var(--card-shadow);
}
@media (max-width: 768px) {
.language-switch {
top: 10px;
right: 10px;
}
}
================================================
FILE: web/static/css/search.css
================================================
.log-search {
position: relative;
margin-bottom: 20px;
}
.log-search input {
padding: 12px 45px;
border-radius: 25px;
font-size: 15px;
font-weight: 500;
border: 2px solid #e9ecef;
transition: all 0.3s ease;
}
.log-search input:focus {
border-color: #4a90e2;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.2);
}
.log-search .clear-search {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #6c757d;
cursor: pointer;
padding: 5px;
transition: all 0.2s ease;
}
.log-search .clear-search:hover {
color: #dc3545;
transform: translateY(-50%) scale(1.1);
}
.log-search i {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
}
================================================
FILE: web/static/css/service-control.css
================================================
.service-control {
height: 100%;
display: flex;
flex-direction: column;
}
.service-control .card-body {
display: flex;
flex-direction: column;
height: 100%;
}
.service-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-color);
margin-bottom: 0.5rem;
display: flex;
align-items: center;
}
.service-description {
font-size: 0.9rem;
color: #666;
margin-bottom: 1rem;
}
.service-actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: auto;
}
.service-btn {
border: none;
border-radius: 6px;
padding: 8px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.service-btn .btn-content {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.service-btn:hover {
transform: translateY(-1px);
filter: brightness(1.1);
}
.service-btn:active {
transform: translateY(1px);
}
@media (max-width: 768px) {
.service-actions {
grid-template-columns: 1fr;
}
.service-btn {
padding: 10px;
}
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.service-title {
color: #e0e0e0;
}
.service-description {
color: #888;
}
}
================================================
FILE: web/static/css/status-card.css
================================================
.status-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 25px;
margin-bottom: 30px;
}
/* 统一卡片基础样式 */
.card.status-card, .card.service-control {
height: 150px;
background: linear-gradient(165deg, #ffffff, #f8f9fa);
border-radius: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.8);
padding: 15px;
margin-bottom: 15px;
}
.card-body {
height: 100%;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
padding: 0;
}
.card.status-card:hover, .card.service-control:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
/* 统一卡片标题样式 */
.card-title, .service-title {
font-size: 1rem;
font-weight: 600;
color: var(--text-color);
margin-bottom: 0.7rem;
display: flex;
align-items: center;
gap: 8px;
}
.card-title i, .service-title i {
font-size: 1.1em;
color: var(--primary-color);
background: rgba(52, 152, 219, 0.1);
padding: 4px;
border-radius: 8px;
}
/* 统一卡片内容样式 */
.card-text {
color: var(--text-color);
font-size: 1.1rem;
font-weight: 500;
margin-bottom: 0.7rem;
flex-grow: 1;
display: flex;
align-items: center;
}
/* 统一按钮容器样式 */
.button-container, .service-actions, .progress-container {
margin-top: auto;
}
/* 统一按钮样式 */
.btn-sm, .service-btn {
padding: 6px 10px;
height: 32px;
font-size: 0.8rem;
border-radius: 8px;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11);
border: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
color: white;
white-space: nowrap;
}
.btn-sm i, .service-btn i {
font-size: 1rem;
}
/* 按钮颜色统一为蓝色 */
.btn-primary,
.service-btn.start-btn,
.service-btn.stop-btn,
.service-btn.restart-btn {
background: linear-gradient(135deg, #3498db, #2980b9);
}
/* 按钮悬停效果 */
.btn-primary:hover,
.service-btn:hover {
transform: translateY(-2px);
box-shadow: 0 7px 14px rgba(50, 50, 93, 0.18);
filter: brightness(1.1);
}
/* 按钮点击效果 */
.btn-primary:active,
.service-btn:active {
transform: translateY(1px);
}
/* 服务控制按钮组样式 */
.service-actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
/* 进度条样式 */
.progress {
height: 5px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.progress-bar {
background: linear-gradient(90deg, var(--primary-color), #5dade2);
box-shadow: 0 0 10px rgba(52, 152, 219, 0.5);
}
/* 卡片图标样式 */
.card-icon {
position: absolute;
top: 15px;
right: 15px;
font-size: 1.2rem;
color: var(--primary-color);
opacity: 0.15;
transform: scale(2);
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.card.status-card, .card.service-control {
background: linear-gradient(165deg, #2d2d2d, #252525);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.card-title, .service-title {
color: #e0e0e0;
}
.card-title i, .service-title i {
background: rgba(52, 152, 219, 0.15);
}
.card-text {
color: #ffffff;
}
.card-icon {
color: var(--primary-color);
opacity: 0.1;
}
.progress {
background: rgba(255, 255, 255, 0.1);
}
.btn-sm, .service-btn {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
.btn-sm:hover, .service-btn:hover {
box-shadow: 0 7px 14px rgba(0, 0, 0, 0.3);
}
}
/* 响应式布局 */
@media (max-width: 768px) {
.status-cards {
grid-template-columns: 1fr;
gap: 20px;
}
.card.status-card, .card.service-control {
height: auto;
min-height: 140px;
}
.service-actions {
margin-top: 0.5rem;
}
.btn-sm, .service-btn {
padding: 6px;
}
}
/* 服务控制卡片特殊样式 */
.service-control .card-body {
padding: 0;
justify-content: space-between;
}
.service-control .service-title {
margin-bottom: 1rem;
font-size: 1.1rem;
}
.service-actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
margin-top: auto;
}
/* 服务控制按钮样式优化 */
.service-btn {
padding: 6px 10px;
min-height: 32px;
font-size: 0.8rem;
}
.service-btn .btn-content {
white-space: nowrap;
}
.service-btn i {
font-size: 1rem;
}
/* 添加信息卡片样式 */
.info-card {
background: linear-gradient(165deg, #ffffff, #f8f9fa);
border-radius: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.8);
padding: 10px;
height: 100%;
min-height: 120px;
}
.info-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
/* 地址容器样式 */
.address-container {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 0.4rem;
}
.address-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.05);
min-height: 28px;
}
.address-label {
display: flex;
align-items: center;
gap: 6px;
font-weight: 500;
color: var(--text-color);
min-width: 80px;
font-size: 0.8rem;
}
.address-label i {
color: var(--primary-color);
font-size: 1rem;
}
.address-value {
flex-grow: 1;
font-family: monospace;
font-size: 0.75rem;
color: var(--text-color);
padding: 2px 4px;
background: rgba(0, 0, 0, 0.03);
border-radius: 4px;
}
.copy-btn {
background: none;
border: none;
color: var(--primary-color);
padding: 3px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 6px;
font-size: 0.85rem;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.copy-btn:hover {
background: rgba(52, 152, 219, 0.1);
transform: scale(1.1);
}
.copy-btn.copy-success {
color: #2ecc71;
background: rgba(46, 204, 113, 0.1);
}
/* 关于信息样式 */
.about-container {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 0.4rem;
}
.about-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.05);
min-height: 28px;
}
.about-item i {
font-size: 1.1rem;
width: 20px;
}
.about-content {
font-size: 0.85rem;
}
.github-link {
color: var(--primary-color);
text-decoration: none;
display: flex;
align-items: center;
gap: 4px;
font-weight: 500;
font-size: 0.85rem;
}
.github-link:hover {
text-decoration: underline;
}
.wechat-name {
font-weight: 500;
color: var(--text-color);
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.info-card {
background: linear-gradient(165deg, #2d2d2d, #252525);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.address-item, .about-item {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.1);
}
.address-value {
background: rgba(0, 0, 0, 0.2);
color: #ffffff;
}
.copy-btn:hover {
background: rgba(52, 152, 219, 0.2);
}
.wechat-name {
color: #e0e0e0;
}
}
/* 响应式布局 */
@media (max-width: 768px) {
.info-card {
margin-bottom: 12px;
min-height: auto;
padding: 10px;
}
.address-item, .about-item {
padding: 6px 8px;
}
}
/* 卡片标题样式优化 */
.info-card .card-title {
font-size: 0.95rem;
margin-bottom: 0.6rem;
gap: 6px;
}
.info-card .card-title i {
font-size: 1rem;
padding: 4px;
}
/* 背景图标优化 */
.info-card .card-icon {
font-size: 1.1rem;
transform: scale(1.8);
top: 12px;
right: 12px;
}
/* 标签页卡片统一样式 */
.tab-pane .card {
background: linear-gradient(165deg, #ffffff, #f8f9fa);
border-radius: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.8);
padding: 20px;
}
.tab-pane .card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
/* 标签页标题样式 */
.tab-pane .card-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-color);
margin-bottom: 1.2rem;
display: flex;
align-items: center;
gap: 8px;
}
.tab-pane .card-title i {
font-size: 1.2em;
color: var(--primary-color);
background: rgba(52, 152, 219, 0.1);
padding: 6px;
border-radius: 8px;
}
/* 标签页背景图标 */
.tab-pane .card-icon {
position: absolute;
top: 20px;
right: 20px;
font-size: 1.5rem;
color: var(--primary-color);
opacity: 0.15;
transform: scale(2.5);
}
/* 按钮统一样式 */
.tab-pane .btn {
padding: 8px 16px;
height: 36px;
font-size: 0.85rem;
border-radius: 8px;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11);
border: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
color: white;
white-space: nowrap;
background: linear-gradient(135deg, #3498db, #2980b9);
}
.tab-pane .btn:hover {
transform: translateY(-2px);
box-shadow: 0 7px 14px rgba(50, 50, 93, 0.18);
filter: brightness(1.1);
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.tab-pane .card {
background: linear-gradient(165deg, #2d2d2d, #252525);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.tab-pane .card-title {
color: #e0e0e0;
}
.tab-pane .card-title i {
background: rgba(52, 152, 219, 0.15);
}
}
/* 代理管理标签页样式 */
.proxy-management {
display: flex;
flex-direction: column;
gap: 1rem;
}
.proxy-actions {
display: flex;
gap: 12px;
margin-top: 1rem;
}
.proxy-actions .btn {
padding: 10px 20px;
font-size: 0.9rem;
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 120px;
justify-content: center;
border-radius: 8px;
transition: all 0.3s ease;
}
.proxy-actions .btn i {
font-size: 1rem;
}
/* IP名单标签页样式 */
.ip-lists-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.ip-lists-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 1rem;
}
.ip-list-section {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.ip-list-title {
font-size: 1rem;
font-weight: 600;
color: var(--text-color);
margin-bottom: 0.5rem;
}
.ip-auth-settings {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.ip-lists-actions {
display: flex;
gap: 12px;
margin-top: 1rem;
}
.ip-lists-actions .btn {
padding: 10px 20px;
font-size: 0.9rem;
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 120px;
justify-content: center;
border-radius: 8px;
transition: all 0.3s ease;
}
/* 按钮悬停效果 */
.proxy-actions .btn:hover,
.ip-lists-actions .btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.ip-list-title {
color: #e0e0e0;
}
.ip-auth-settings {
border-top-color: rgba(255, 255, 255, 0.1);
}
}
/* 代理列表区域样式 */
.proxy-lists {
background: rgba(255, 255, 255, 0.5);
border-radius: 12px;
padding: 15px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.proxy-list-section {
margin-bottom: 1rem;
}
.proxy-list-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-color);
}
.proxy-list-header i {
color: var(--primary-color);
font-size: 1.1rem;
}
.proxy-list-help {
margin-left: auto;
font-size: 0.85rem;
color: #666;
font-weight: normal;
}
.proxy-list-section textarea {
font-family: monospace;
font-size: 0.9rem;
resize: vertical;
background: rgba(255, 255, 255, 0.8);
}
.proxy-list-section textarea:focus {
background: white;
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.proxy-lists {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.1);
}
.proxy-list-header {
color: #e0e0e0;
}
.proxy-list-help {
color: #888;
}
.proxy-list-section textarea {
background: rgba(0, 0, 0, 0.2);
color: #e0e0e0;
}
.proxy-list-section textarea:focus {
background: rgba(0, 0, 0, 0.3);
}
}
/* 地址显示样式 */
.address-value {
font-family: monospace;
background: rgba(0, 0, 0, 0.05);
padding: 6px 10px;
border-radius: 4px;
font-size: 0.9rem;
color: var(--text-color);
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.address-value {
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.copy-btn:hover {
background: rgba(52, 152, 219, 0.2);
}
.copy-btn.copy-success {
background: rgba(46, 204, 113, 0.2);
}
}
/* 服务状态样式 */
.service-status {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 0.7rem;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
transition: background-color 0.3s ease;
}
.status-dot.running {
background-color: #2ecc71;
box-shadow: 0 0 10px rgba(46, 204, 113, 0.5);
animation: pulse 2s infinite;
}
.status-dot.stopped {
background-color: #e74c3c;
box-shadow: 0 0 10px rgba(231, 76, 60, 0.5);
animation: none;
}
.status-text {
font-weight: 600;
color: var(--text-color);
transition: color 0.3s ease;
}
/* 添加脉冲动画 */
@keyframes pulse {
gitextract_q50giu6l/
├── .gitattributes
├── Dockerfile
├── LICENSE
├── ProxyCat-Manual/
│ ├── Investigation Manual.md
│ ├── Operation Manual.md
│ └── logs.md
├── ProxyCat.py
├── README-EN.md
├── README.md
├── app.py
├── config/
│ ├── blacklist.txt
│ ├── config.ini
│ ├── getip.py
│ ├── ip.txt
│ └── whitelist.txt
├── docker-compose.yml
├── logs/
│ └── proxycat.log
├── modules/
│ ├── modules.py
│ └── proxyserver.py
├── requirements.txt
└── web/
├── static/
│ └── css/
│ ├── animations.css
│ ├── base.css
│ ├── buttons.css
│ ├── dark-mode.css
│ ├── forms.css
│ ├── logs.css
│ ├── nav-tabs.css
│ ├── progress.css
│ ├── responsive.css
│ ├── search.css
│ ├── service-control.css
│ └── status-card.css
└── templates/
└── index.html
SYMBOL INDEX (106 symbols across 5 files)
FILE: ProxyCat.py
function setup_logging (line 14) | def setup_logging():
function update_status (line 21) | def update_status(server):
function handle_client_wrapper (line 134) | async def handle_client_wrapper(server, reader, writer, clients):
function run_server (line 144) | async def run_server(server):
function run_proxy_check (line 155) | async def run_proxy_check(server):
class ProxyCat (line 169) | class ProxyCat:
method __init__ (line 170) | def __init__(self):
method start_server (line 231) | async def start_server(self):
method handle_shutdown (line 254) | def handle_shutdown(self, signum, frame):
method handle_client (line 260) | async def handle_client(self, reader, writer):
method _authenticate (line 287) | def _authenticate(self, auth_header):
method _handle_proxy_request (line 303) | async def _handle_proxy_request(self, reader, writer):
method _handle_connect (line 323) | async def _handle_connect(self, target, reader, writer):
method _handle_http (line 343) | async def _handle_http(self, method, target, version, reader, writer):
method _create_pipe (line 373) | async def _create_pipe(self, client_reader, client_writer, remote_read...
method _pipe (line 395) | async def _pipe(self, reader, writer):
method monitor_resources (line 410) | def monitor_resources(self):
FILE: app.py
function get_config_path (line 31) | def get_config_path(filename):
class CustomFormatter (line 39) | class CustomFormatter(logging.Formatter):
method formatTime (line 40) | def formatTime(self, record, datefmt=None):
function setup_logging (line 43) | def setup_logging():
class MemoryHandler (line 63) | class MemoryHandler(logging.Handler):
method emit (line 64) | def emit(self, record):
function require_token (line 74) | def require_token(f):
function root (line 93) | def root():
function web (line 101) | def web():
function get_status (line 106) | def get_status():
function save_config (line 145) | def save_config():
function handle_proxies (line 197) | def handle_proxies():
function check_proxies_api (line 227) | def check_proxies_api():
function handle_ip_lists (line 245) | def handle_ip_lists():
function get_logs (line 279) | def get_logs():
function clear_logs (line 311) | def clear_logs():
function switch_proxy (line 331) | def switch_proxy():
function control_service (line 353) | def control_service():
function change_language (line 461) | def change_language():
function check_version (line 494) | def check_version():
function handle_users (line 540) | def handle_users():
function send_static (line 596) | def send_static(path):
function run_proxy_server (line 599) | def run_proxy_server():
FILE: config/getip.py
function newip (line 5) | def newip():
FILE: modules/modules.py
class ColoredFormatter (line 6) | class ColoredFormatter(logging.Formatter):
method format (line 14) | def format(self, record):
class MessageManager (line 368) | class MessageManager:
method __init__ (line 369) | def __init__(self, messages=MESSAGES):
method get (line 373) | def get(self, key, lang='cn', *args):
function print_banner (line 382) | def print_banner(config):
function logos (line 464) | def logos():
function load_config (line 483) | def load_config(config_file='config/config.ini'):
function load_ip_list (line 505) | def load_ip_list(file_path):
function parse_proxy (line 518) | def parse_proxy(proxy):
function check_http_proxy (line 533) | async def check_http_proxy(proxy, test_url=None):
function check_socks_proxy (line 559) | async def check_socks_proxy(proxy, test_url=None):
function check_proxy (line 609) | async def check_proxy(proxy, test_url=None):
function check_proxies (line 637) | async def check_proxies(proxies, test_url=None):
function check_for_updates (line 644) | async def check_for_updates(language='cn'):
FILE: modules/proxyserver.py
function load_proxies (line 9) | def load_proxies(file_path='ip.txt'):
function validate_proxy (line 13) | def validate_proxy(proxy):
class AsyncProxyServer (line 22) | class AsyncProxyServer:
method __init__ (line 23) | def __init__(self, config):
method _init_config_values (line 30) | def _init_config_values(self, config):
method _init_server_state (line 71) | def _init_server_state(self):
method _init_connection_settings (line 88) | def _init_connection_settings(self):
method _update_config_values (line 101) | def _update_config_values(self, new_config):
method _handle_mode_change (line 107) | def _handle_mode_change(self):
method _check_proxies_wrapper (line 139) | async def _check_proxies_wrapper(self):
method _reload_proxies (line 143) | def _reload_proxies(self):
method _check_proxies (line 169) | async def _check_proxies(self):
method _load_file_proxies (line 177) | def _load_file_proxies(self):
method start (line 191) | async def start(self):
method stop (line 242) | async def stop(self):
method get_next_proxy (line 259) | async def get_next_proxy(self):
method _load_getip_proxy (line 306) | async def _load_getip_proxy(self):
method time_until_next_switch (line 318) | def time_until_next_switch(self):
method check_ip_auth (line 321) | def check_ip_auth(self, ip):
method _authenticate (line 344) | def _authenticate(self, headers):
method _close_connection (line 368) | async def _close_connection(self, writer):
method handle_client (line 381) | async def handle_client(self, reader, writer):
method _pipe (line 414) | async def _pipe(self, reader, writer):
method _split_proxy_auth (line 441) | def _split_proxy_auth(self, proxy_addr):
method _create_client (line 451) | async def _create_client(self, proxy):
method _cleanup_connections (line 482) | async def _cleanup_connections(self):
method handle_socks5_connection (line 492) | async def handle_socks5_connection(self, reader, writer):
method _initiate_socks5 (line 616) | async def _initiate_socks5(self, remote_reader, remote_writer, dst_add...
method _initiate_http (line 713) | async def _initiate_http(self, remote_reader, remote_writer, dst_addr,...
method _handle_client_impl (line 726) | async def _handle_client_impl(self, reader, writer, first_byte):
method _handle_connect (line 791) | async def _handle_connect(self, path, reader, writer):
method _handle_request (line 915) | async def _handle_request(self, method, path, headers, reader, writer):
method _get_client (line 1051) | async def _get_client(self, proxy):
method handle_proxy_failure (line 1077) | async def handle_proxy_failure(self):
method switch_proxy (line 1112) | async def switch_proxy(self):
method check_current_proxy (line 1148) | async def check_current_proxy(self):
method _clean_proxy_cache (line 1193) | def _clean_proxy_cache(self):
method initialize_proxies (line 1206) | def initialize_proxies(self):
method cleanup_disconnected_ips (line 1227) | async def cleanup_disconnected_ips(self):
method is_docker (line 1236) | def is_docker():
method get_proxy_status (line 1239) | async def get_proxy_status(self):
method _get_proxy_connection (line 1250) | async def _get_proxy_connection(self, proxy):
method _create_socks_connection (line 1285) | async def _create_socks_connection(self, host, port, username, passwor...
method _create_http_connection (line 1311) | async def _create_http_connection(self, host, port, username, password):
method _cleanup_pool (line 1324) | async def _cleanup_pool(self):
method _log_proxy_switch (line 1343) | def _log_proxy_switch(self, old_proxy, new_proxy):
method _validate_proxy (line 1357) | async def _validate_proxy(self, proxy):
method get_proxy (line 1393) | async def get_proxy(self):
method cleanup_clients (line 1435) | async def cleanup_clients(self):
method get_active_connections (line 1452) | def get_active_connections(self):
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (335K chars).
[
{
"path": ".gitattributes",
"chars": 93,
"preview": "*.js linguist-language=python\n*.css linguist-language=python\n*.html linguist-language=python\n"
},
{
"path": "Dockerfile",
"chars": 321,
"preview": "FROM python:3.11\n\nWORKDIR /app\n\nCOPY requirements.txt .\n\nRUN pip install --upgrade pip -i https://pypi.mirrors.ustc.edu."
},
{
"path": "LICENSE",
"chars": 18092,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 2, June 1991\n\n Copyright (C) 1989, 1991 Fr"
},
{
"path": "ProxyCat-Manual/Investigation Manual.md",
"chars": 408,
"preview": "Q:出现不正常代理地址,无法正常使用代理\n\n\n\nA:检查 API 地址所提供的代理格式是否正确,是否给自己出口IP加白了。\n\n\n\nQ:为什么运行 Prox"
},
{
"path": "ProxyCat-Manual/Operation Manual.md",
"chars": 5415,
"preview": "# ProxyCat 使用手册\n\n## 重要事项\n\n- Python版本最好为Python 3.11\n- Releases中为较为稳定的打包版本,不一定是最新\n- API 接口所获取的代理地址必须为 IP:PORT 格式且只提供一条地址\n\n"
},
{
"path": "ProxyCat-Manual/logs.md",
"chars": 2454,
"preview": "### 2025/03/23\n\n- 修复负载均衡模式无法调用的BUG\n- 修复socks5连接错误\n- 修复http、socks5监听下的目标网站错误和代理地址失效情况一致导致无法正常触发代理切换\n- 修改代理有效性校验,配置为可控检测,关"
},
{
"path": "ProxyCat.py",
"chars": 17719,
"preview": "from wsgiref import headers\nfrom modules.modules import ColoredFormatter, load_config, DEFAULT_CONFIG, check_proxies, ch"
},
{
"path": "README-EN.md",
"chars": 5776,
"preview": ":\n config = load_config("
},
{
"path": "config/ip.txt",
"chars": 45,
"preview": "http://127.0.0.1:7890\nsocks5://127.0.0.1:7890"
},
{
"path": "config/whitelist.txt",
"chars": 0,
"preview": ""
},
{
"path": "docker-compose.yml",
"chars": 237,
"preview": "version: '3'\nservices:\n proxycat:\n build: .\n environment:\n - TZ=Asia/Shanghai\n ports:\n - \"1080:1080\""
},
{
"path": "logs/proxycat.log",
"chars": 0,
"preview": ""
},
{
"path": "modules/modules.py",
"chars": 28478,
"preview": "import asyncio, logging, random, httpx, re, os, time\nfrom configparser import ConfigParser\nfrom packaging import version"
},
{
"path": "modules/proxyserver.py",
"chars": 60756,
"preview": "import asyncio, httpx, logging, re, socket, struct, time, base64, random, os\nfrom modules.modules import get_message, lo"
},
{
"path": "requirements.txt",
"chars": 166,
"preview": "colorama==0.4.6\nhttpx==0.27.2\nhttpx[http2,socks]==0.27.2\npackaging==24.1\nRequests==2.32.3\ntqdm>=4.65.0\nflask>=2.0.1\nwerk"
},
{
"path": "web/static/css/animations.css",
"chars": 469,
"preview": "@keyframes slideIn {\n from { transform: translateY(-20px); opacity: 0; }\n to { transform: translateY(0); opacity: "
},
{
"path": "web/static/css/base.css",
"chars": 1095,
"preview": ":root {\n --primary-color: #3498db;\n --secondary-color: #2ecc71;\n --warning-color: #f1c40f;\n --danger-color: "
},
{
"path": "web/static/css/buttons.css",
"chars": 724,
"preview": ".btn {\n border: none;\n border-radius: 8px;\n padding: 10px 20px;\n font-weight: 500;\n cursor: pointer;\n "
},
{
"path": "web/static/css/dark-mode.css",
"chars": 1278,
"preview": "@media (prefers-color-scheme: dark) {\n :root {\n --background-color: #1a1a1a;\n --text-color: #ffffff;\n "
},
{
"path": "web/static/css/forms.css",
"chars": 4099,
"preview": ".form-control, .form-select {\n border-radius: 8px;\n border: 2px solid #e9ecef;\n padding: 10px 15px;\n transit"
},
{
"path": "web/static/css/logs.css",
"chars": 1960,
"preview": ".log-container {\n height: 500px;\n position: relative;\n margin: 1rem 0;\n}\n\n.log-INFO { \n background-color: #e"
},
{
"path": "web/static/css/nav-tabs.css",
"chars": 589,
"preview": ".nav-tabs {\n border-bottom: 2px solid #dee2e6;\n margin-bottom: 20px;\n}\n\n.nav-tabs .nav-link {\n border: none;\n "
},
{
"path": "web/static/css/progress.css",
"chars": 874,
"preview": ".progress-container {\n margin-top: auto;\n}\n\n.progress {\n height: 8px;\n border-radius: 10px;\n background: rgb"
},
{
"path": "web/static/css/responsive.css",
"chars": 648,
"preview": "@media (max-width: 768px) {\n .status-card {\n margin-bottom: 20px;\n }\n\n .btn {\n width: 100%;\n "
},
{
"path": "web/static/css/search.css",
"chars": 825,
"preview": ".log-search {\n position: relative;\n margin-bottom: 20px;\n}\n\n.log-search input {\n padding: 12px 45px;\n border"
},
{
"path": "web/static/css/service-control.css",
"chars": 1310,
"preview": ".service-control {\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.service-control .card-body {\n "
},
{
"path": "web/static/css/status-card.css",
"chars": 15722,
"preview": ".status-cards {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 25px;\n margin-bottom: 30px;\n}\n"
},
{
"path": "web/templates/index.html",
"chars": 125398,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-wi"
}
]
About this extraction
This page contains the full source code of the honmashironeko/ProxyCat GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 33 files (315.1 KB), approximately 75.0k tokens, and a symbol index with 106 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.