Python wss 403 error
Tags: python, websocket, cloudflare
Published: 2023-06-21 11:39:10

### 背景

---

- 爬取dexscreener的数据，发现是wss通信的，于是乎就用了下面的代码实现
```
import time
import websocket
import ssl
import os
import base64
import json
import logging
from math import ceil
from utils.redisdb import redis_cli

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger('CMC MONITOR14')



class ListPage:
    def __init__(self):
        self.base_url = 'wss://io.dexscreener.com/dex/screener/pairs/h24/{}?rankBy[key]=txns&rankBy[order]=desc&filters[liquidity][min]=100000&filters[marketCap][min]=200000&filters[txns][h24][min]=100'
        self.base_url = 'wss://io.dexscreener.com/dex/screener/pairs/h24/1?rankBy[key]=txns&rankBy[order]=desc&filters[liquidity][min]=100000&filters[marketCap][min]=200000&filters[txns][h24][min]=100'
    

def generate_websocket_key(self):
        # 生成一个16字节的随机值
        buffer = os.urandom(16)
        # 将随机值进行Base64编码
        key = base64.b64encode(buffer).decode('utf-8')
        return key

    def open_connection(self, num):
        """
        建立连接
        """
        header = {
                # 用户唯一性校验+风控识别校验，可以理解为mac地址的作用，该值有一定的规范，是随机生成的，但是得符合一定的标准，在标准之外的随机数是不被认可的，无法建立连接
                "Sec-WebSocket-Key": self.generate_websocket_key(),
                # 和http请求的用途一样，用于校验客户端角色；主要风控
                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
            }

        ws = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
        # remote = 'wss://io.dexscreener.com/dex/screener/pairs/h24/4??rankBy[key]=txns&rankBy[order]=desc&filters[liquidity][min]=100000&filters[marketCap][min]=200000&filters[txns][h24][min]=100'
        url = self.base_url.format(num)
        logger.info(f'request {num}: {url}')
        # 必须得携带头部信息，Python代码实现的wss通信自动生成的Sec-WebSocket-Key 很多服务器会不支持
        ws.connect(url, header=header)
        return ws
```
- 一开始上面的代码并没有问题，能够正常的请求成功，但是后来不行了，应该是目标站站点进行了某些升级，导致代码失效
- 在失效后耗时差不多3天左右，一直在请求头的各个参数伪造徘徊，包括cookie、Sec-WebSocket-Key、等等；也怀疑是TLS问题，基于ja3指纹又做了很多尝试依然无果
- 后来有其他项目插进来，中间停滞了有好长一段时间；待其他项目开发完之后，花了一天的时间，调试、查资料，才找到点蛛丝马迹（关于这个问题，中文区和英文区能够检索的资料太少了，这就导致问题解决推进艰难）；
- 偶然间发现了这个 https://github.com/websocket-client/websocket-client/issues/489 issue，于是谜底开始揭晓

### 解决方案

---

- 基于上面发现的issue，基本能够判断是`suppress_origin`的问题，也就是下面的方法需要这么传入该参数，才能绕过：
```
ws = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
ws.connect(url,header=header,suppress_origin=True)
```
- 这里是websocket库的官方文档关于这个参数的解释：https://websocket-client.readthedocs.io/en/latest/examples.html#suppress-origin-header
- 到这里是解决了，下面是完整的代码：
```
header ={
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
    "Origin": "https://dexscreener.com",
}


url = 'wss://io.dexscreener.com/dex/screener/pairs/h24/2?rankBy%5Bkey%5D=txns&rankBy%5Border%5D=desc'
import ssl
import websocket
websocket.enableTrace(True)
ws = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
ws.connect(url,header=header,suppress_origin=True)
ws.pong()
result = ws.recv()
print(result)
```
- 虽然官方文档字面意思能看懂，但是依然是似懂非懂，没明白这么做的原理是啥
- 问了GPT-4，下面是GPT-4的答案：

  > suppress_origin 是一个可选的参数，用于 Python WebSocket 服务器库，如 websockets。当设置为 True 时，它会使服务器在处理 WebSocket 握手请求时，忽略 Origin 头部。Origin 头部是浏览器在发起跨域请求时会自动添加的一个 HTTP 头部，用于说明请求是从哪个源（协议、域名和端口）发起的。
在 WebSocket 协议中，服务器可以使用 Origin 头部来判断客户端是否来自合法的源。这有助于防止跨站请求伪造（CSRF）攻击，这是一种攻击者利用受害者的身份向服务器发送恶意请求的攻击方式。

- 上面提到的是`同源策略`，大概意思是只有浏览器才能成功，Python代码伪造即使是在header中手动指定origin这个参数，自定义也不会生效；
- 而使用 suppress_origin=True 参数是关闭同源策略的限制