Flask搭建代理服务HTTP正常HTTPS请求报404错误
问题根因
报404的核心原因非常明确,共三个点:
- HTTPS请求走正向代理时,客户端不会直接发GET/POST这类普通请求,第一步会先发
CONNECT方法的请求,要求代理和目标站点建立TCP隧道,隧道连通后才会在加密通道里传输实际HTTPS请求。你写的Flask路由只绑定了GET/POST/DELETE/PATCH/PUT这几个方法,根本没处理CONNECT方法,请求进来Flask找不到对应路由直接返回404,正好对应报错里的Tunnel connection failed: 404 NOT FOUND。 - Flask本身是WSGI框架,设计初衷是做普通Web应用的请求-响应处理,根本不支持正向代理需要的裸TCP隧道透传。就算你给路由加上CONNECT方法,Flask的请求上下文模型也没法直接实现双向字节流转发的隧道逻辑,硬改成本极高。
- 你代码本身存在逻辑错误:给Flask加了
ssl_context参数把代理本身做成HTTPS服务,又在转发时直接拿request.url当目标地址,HTTPS代理场景下这个值根本不是完整的目标站点地址,逻辑从根上就不对。
修复方案
先明确一个前提:纯Flask实现不了完整的正向代理HTTPS转发能力,WSGI协议的模型天生不适合做这个,别在路由注册上绕弯路,选下面两种可行路径:
方案1:基于底层TCP能力自行实现
放弃Flask,直接用原生socket或者支持底层TCP处理的库(gevent、asyncio都可以)写代理逻辑,核心流程:
- 监听端口收到客户端连接后,先读取请求第一行判断类型
- 如果是普通HTTP请求(方法为GET/POST等,路径是完整
http://开头的URL),按原有逻辑转发即可 - 如果是CONNECT请求,解析出目标站点的主机、端口,和目标建立TCP连接后,给客户端返回
200 Connection Established响应,之后直接双向透传两个连接之间的字节流,不需要解析中间的加密内容
- 如果是普通HTTP请求(方法为GET/POST等,路径是完整
- 如果需要代理本身走HTTPS(也就是客户端配置的代理地址是
https://开头),先给客户端连接完成TLS握手,再走上面的请求判断、转发流程。
最简的socket实现CONNECT隧道的示例代码:
import socket import threading def transfer(src, dst): try: while True: buff = src.recv(8192) if not buff: break dst.sendall(buff) except Exception: pass finally: src.close() dst.close() def handle_conn(client): try: head_data = client.recv(8192) if not head_data: client.close() return method, target, _ = head_data.split(b' ')[:3] if method == b'CONNECT': host, port = target.decode().split(':') port = int(port) remote = socket.create_connection((host, port)) client.send(b'HTTP/1.1 200 Connection Established\r\n\r\n') threading.Thread(target=transfer, args=(client, remote)).start() threading.Thread(target=transfer, args=(remote, client)).start() else: # 普通HTTP请求转发逻辑自行补充即可 client.close() except Exception: client.close() if __name__ == '__main__': listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listen_sock.bind(('0.0.0.0', 39100)) listen_sock.listen(128) while True: conn, _ = listen_sock.accept() threading.Thread(target=handle_conn, args=(conn,)).start()
如果一定要复用Flask相关的依赖,可以基于werkzeug的底层服务器自定义请求处理逻辑,拦截CONNECT方法做透传,但本质还是绕开Flask的路由体系,不如直接写socket省事。
方案2:直接使用成熟代理组件
如果不需要在代理里加自定义业务逻辑,不用自己造轮子,直接用现成的正向代理方案即可,比如Nginx配置正向代理规则、用mitmproxy这类工具,稳定性比自己写的高很多。
现有代码的其他问题
- 转发请求时直接透传全部请求头,其中
Host字段是代理服务的地址,不是目标站点地址,就算HTTP请求能通,遇到做Host校验的站点也会报错 - 异常捕获只覆盖了
ProxyError,实际转发时会遇到超时、连接拒绝、证书错误等各类异常,都会直接抛出500错误 - 代理本身配置了自签SSL证书,测试时就算隧道逻辑通了,客户端如果没信任这个自签证书,和代理建立TLS连接时就会报错,你测试代码里的
verify=False只会跳过目标站点的证书校验,不会跳过代理本身的证书校验。
内容的提问来源于stack exchange,提问作者Anters Bear
相关产品推荐
相关产品推荐

