分类 Web 相关 下的文章

selenium 截屏 设置带用户名密码的代理和注入cookie

今天想对一个metrics 数据做截屏自动保存, 想到使用 selenium 来做. 于是让 chatGPT 给写了一个脚本. 发现其中有2处需要注意的地方.

  1. 一般的设置代理方法(selenium.webdriver.common.proxy不包含代理的用户名密码)不管用, 必须使用 seleniumwire.
  2. 注入 cookie. 为什么不通过登录获得 cookie 而使用注入? 默认的登录使用 2FA, 其中需要MAC 的指纹, 所以很麻烦, 索性一开始先从浏览器拿一个cookie注入.

python 代码如下:

from selenium import webdriver
from seleniumwire import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.proxy import Proxy, ProxyType
from PIL import Image
import time

def init_chrome_driver():
    # 设置代理
    options = {
        'proxy': {
            'http': 'http://user:password@proxy.tianxiaohui.com:8080', 
            'https': 'http://user:password@proxy.tianxiaohui.com:8080',
            'no_proxy': 'localhost,127.0.0.1' # excludes
        }
    }

    # 设置Chrome选项
    chrome_options = Options()
    chrome_options.add_argument("--headless")  # 无头模式
    chrome_options.add_argument("--disable-gpu")
    chrome_options.add_argument("--window-size=1920x1080")

    # 设置Chrome驱动路径
    chrome_driver_path = '/Users/supra/work/tools/chromedriver-mac-x64/chromedriver'  # 请替换为你的chromedriver路径

    # 初始化Chrome驱动
    service = Service(chrome_driver_path)
    driver = webdriver.Chrome(service=service, options=chrome_options, seleniumwire_options=options)

    return driver

def setup_driver(driver):
    # 打开一个空白页
    driver.get('http://mydash.tianxiaohui.com/?orgId=1')
    # 等待页面加载完成
    time.sleep(5)

    # 添加现有的cookie
    cookies = [
        {
            'name': 'grafana_session',
            'value': '5c74f90284d363050b63c20c86a2d336',
            'path': '/',
            'domain': 'mydash.tianxiaohui.com'
        }
    ]

    for cookie in cookies:
        driver.add_cookie(cookie)

    return driver

def take_screenshot(driver, url, screenshot_abs_file):
    # 打开网址
    driver.get(url)
    # 等待页面加载完成
    time.sleep(30)
    # 截图并保存到本地
    driver.save_screenshot(screenshot_abs_file)

    return driver

driver = init_chrome_driver()
driver = setup_driver(driver)
url = 'http://mydash.tianxiaohui.com/d/test_dash'
print(url)
screenshot_path = f'/Users/supra/Downloads/screenshot.png'
driver = take_screenshot(driver, url, screenshot_path)

# 关闭浏览器
driver.quit()

# 使用Pillow库打开截图并显示
#image = Image.open(screenshot_path)
#image.show()

http status code 431

这周末处理一个 http status code 431 的 case. 431 表示 header 字段的数据太大, 听起来也没那么复杂, 就是消减一下 header 内容就解决了. 但是在大家都说最近没有任何改动的情况下, 到底哪里出问题了呢?

431 表示什么?

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431 介绍看. 有 2 种可能的情况:

  1. 所有 header 的字段数据加起来太大了;
  2. 单个 header 的字段数据太大了;

谁会报这个错?

听起来这个问题有点多余, 当然是应用服务器了. 其实不然. 中间可以拆包的所有网络组件都有可能, 比如: 可以拆包的代理, 负载均衡服务器, 如今应用广泛的反向代理服务器 envoy, nginx 等. 他们都可以拆开 http 的报, 检查一下 header 的大小, 然后报个 431 给客户端.

例子

下面便是当时的一个例子, 把其中一个字段加大到60多KB. 可以看到 header 是431, response payload 是一句话.
这个例子是 envoy 拆包后发现 header 很大, 根据 envoy 官方文档: max_request_headers_kb, 它不会验证单个 header 大小, 而是验证整个header 大小. 默认是60K, 所以这里超了.
header.png

网络链路添加 header

通常情况下, 做为客户端发出的所有 header 就是服务器端收到的所有 header, 但是这些中间会拆包的各个组件其实都有可能添加新的 header 到原始的数据报文中. 比如: 一般负载均衡服务器会添加一个 header 告诉下游服务器真正的客户端的 IP 是多少, 这样下游服务器就知道真正的客户IP 是多少了? 否则它通过 remote IP 获得的就是负载均衡服务器的IP.

有时候, 中间拆包的组件可能会添加更多的 header 用来输送更多的信息给下游, 比如: 当前拆包的组件名称, 它的IP, 它的版本之类的.

如何区分到底是最终应用服务器返回431 还是中间拆包组件?

通常情况下, 这些不同的组件或者应用服务器返回的 response header 里面会标明它的身份. 比如我们的例子中它就是中间的 envoy 组件返回的, 不是最终的应用服务器. 应用服务器会返回带不同 header response.

为什么 header 会很大?

各种原因都会有, 比如:

  1. Referer 是一个巨长的 URL.
  2. Cookie 的内容太多 (Cookie 的内容都是header里面的)
  3. 某个 header 添加了巨多内容.

具体在我们这个case 里面, 就是客户端有个小系统, 它为了不让下游子系统去到一个远程系统取很多数据, 直接把这部份内容放到 header 发给下游了, 然而这部份内容是一个动态变动的部分, 就像一个list, 会随着系统增加减小, 正巧它这次很大, 就遇到这种情况.

使用CSS伪元素和动画实现光标闪烁效果

当用户在 chatGPT 输入问题之后, 并且chatGPT 尚没有返回的时候, 我们会看到有个黑色的圆点在闪烁. 如何实现这么一个闪烁的圆点?

以下是现实代码:

.toggle-text::after {
    font-size: 12px;
    animation: toggle 1s infinite;
    content: '⚫';
  }

  @keyframes toggle {
    0%, 50% {
      opacity: 0;
    }
    50.01%, 100% {
      opacity: 1;
    }
  }

<div class='toggle-text'>text</div>

这里面用到的技术

  1. CSS 伪元素: 这里的 ::after 就是CSS 伪元素, 它们都不是真实存在的元素. 它不属于对应的html 元素, 而是在它之后, 当然还有很多其它伪元素.
  2. CSS 动画: 这里通过 animation 来实现动画. 动画的帧通过配合 @keyframes 来完成.
  3. @keyframes 通过 opacity 来实现透明度, 造成闪烁的效果.
  4. 这里插入的是 ⚫, 其实它是一个 unicode 字符, 还有更小的点, 也可以给他改变font 大小, 颜色等. 还有其它各种 unicode 表示的点. 看这里: https://www.unicodepedia.com/groups/geometric-shapes/

http keep-alive 实验

之前一篇讲道客户端和服务端是如何处理 http keep-alive 的, 其中很多都是一笔带过. 本篇补充一些细节.

版本

对于 http keep-alive 的概念, 这里的讨论只局限于 http 1.0, http 1.1. 对于 HTTP/2, HTTP/3 这里的讨论不适用.

http header - Connection & Keep-Alive

http 1.1 里面默认是持久连接. 但是我们可以看到下面的默认情况:
Chrome:

  1. 默认发送 Connection: keep-alive 头, 但是不发 Keep-Alive.

curl:

  1. Ubuntu 上的 curl 7.81.0 默认连 Connection 都没发.
  2. Mac 上的 curl 8.6.0 也没发 Connection header.

python requests:

GET / HTTP/1.1
Host: www.henu.edu.cn
User-Agent: python-requests/2.31.0
Connection: keep-alive

Java:

GET / HTTP/1.1
User-Agent: Java/17.0.4.1
Host: www.henu.edu.cn
Connection: keep-alive

JDK 里面的 Java client 默认的处理细节

Keep-Alive header 里面的 timeout 和 max 分别对应 JDK HttpClient 里面的字段:
timeout - keepAliveTimeout
max - keepAliveConnections.

keepAliveTimeout

首先, 在 HttpClient 里面定义了一个 keepAliveTimeout 字段. JDK 21 到链接:
https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L136C9-L136C25.

这个字段有4种取值可能:

  1. 正值 - timeout 的秒数, 对应 Keep-Alive header 里面的 timeout 值.
  2. 0 - 对方明确不需要 Keep-Alive.
  3. -1: 需要保持连接, http 1.1 设置或者不设 Connection: keep-alive, 但是没有设置 timeout 值.
  4. -2: 明确在 Keep-alive header 里面设置了 timeout: 0 这个值.

如果把我们上面实验的客户端的结果反过来看成对方发来的response 来看, 都属于上面的第3种: -1 类型.

真实的JDK java 客户端的解析过程在这些代码中:
https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L907-L917

真正的使用这个值的地方

真正使用这个值的地方在:
https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/net/www/http/KeepAliveCache.java#L162-L177

keepAliveConnections

这个值全部都是在 HttpClient 里面使用的.

parse:

  1. 若头部带来了 Keep-Alivemax 则使用这个值.
  2. 若没带来 max, 则如果使用代理, 则是50, 否则是5.
    细节代码: https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L902-L904

WebSocket 协议

最近有个项目要做一个Slack Robot, 就是根据用户发到Slack 某个Channel的内容, 给出智能回复.

与Slack的交互方式有2种: HTTP连接的方式和WebSocket的方式. HTTP 连接的方式又有2种实现方式:

  1. 我们提供一个公开的endpoint, 当Slack的Channel 有消息的时候, Slack 以webhook的方式及时通知我们, 我们的app 给出回复.
  2. 我们不提供endpoint, 我们连接Slack的endpoint, 以固定时间间隔的方式去poll 消息, 如果有新消息, 我们的app 给出回复.

以上这2种方式都不是很好的方式, 对于第一种, 我们要提供一个公开的endpoint, 在我们的生产环境基本不可能, 对于第二种方式, 不是基于事件的, 不管有没有消息, 都要固定的去poll, 对于一个app 里面有多个实例的情况, 还要控制那个实例去poll.

所以 WebSocket 的方式是最好的, 不过当时就有开发人员提出了意见: 我们的生产环境时不能连外网的, 要连只能通过http 代理, 而我们的生产环境的代理只支持 http 代理, 不支持sock 方式, 所以这种 WebSocket 行不通. 是真的吗?

在我们看看 WebSocket 到底是什么之前, 先做一个 WebSocket 的例子: 使用Node.js 开一个WebSocket 服务, 然后在浏览器开一个 WebSocket 客户端.

WebSocket 服务端

  1. 新建一个文件夹

    mkdir wsServer
    cd wsServer
  2. 新建一个 package.json

    vim package.json

    输入如下代码

    {
     "name": "wsServer",
     "version": "0.0.1",
     "type": "module",
     "dependencies": {
       "ws":"8.8.1"
     }
    }
  3. 新建一个服务端代码文件

    vim server.js

    输入如下代码:

    var WebSocketServer = require('ws').Server
    var fs = require('fs')
    
    const wss = new WebSocketServer({ port: 8080 });
    
    wss.on('connection', function connection(ws) {
     ws.on('message', function message(data) {
     console.log('received: %s', data);
     ws.send(data + " -> server ack");
     });
    
     fs.watch('/tmp/', (eventType, fileName) => {
     console.log('Get event on file ' + fileName + ', type: ' + eventType);
     ws.send('file -> ' + fileName + ' -> ' + eventType);
     })
    
     ws.send('welcome to WebSocket world!');
    });

    上面这段代码做3件事情:

  4. 启动 WebSocket 服务器在8080 端口上, 当有人来连接的时候, 发送欢迎消息;
  5. 当收到客户端消息的时候, 打印收到的消息, 并且发送给客户端 ack 消息;
  6. 监听本地 /tmp 文件夹的文件变动事件, 打印日志, 并推送给客户端;

启动服务端代码:

node server.js

WebSocket 客户端

在 Chrome 浏览器打开任意页面的控制台, 输入如下JavaScript 代码

const ws = new WebSocket('ws://10.249.64.103:8080');

// Listen for messages
ws.addEventListener('message', (event) => {
    console.log('Message from server: ', event.data);
});

document.addEventListener('click', (event) => {
    console.log("just clicked");
    ws.send('click on (' + event.x + ', ' + event.y + ')');
});

上面的代码做下面的事情:

  1. 连接服务端 WebSocket;
  2. 然后当收到服务器端消息的时候, 打印收到的消息;
  3. 当页面上收到点击事件的时候, 推送给服务端点击事件的坐标;

运行效果

客户端效果:
client.png

服务器端效果:
server.png

使用代理

因为客户端使用的是浏览器, 可以设置在chrome 设置对于这个IP 启用代理, 使用http 代理, 依然能联通server 端, 正常运转.

使用curl

使用curl 只能被动的接受服务器传来的消息, 不能发送任何消息. (这个 Sec-WebSocket-Key 是从浏览器刚才发送的历史中复制过来的)

curl \
    --include \
    --no-buffer \
    --header "Connection: Upgrade" \
    --header "Upgrade: websocket" \
    --header "Host: 10.249.64.103:8080" \
    --header "Origin: http://10.249.64.103:8080" \
    --header "Sec-WebSocket-Key: 1TFTcjPQ7iG2XvsZ83WgZg==" \
    --header "Sec-WebSocket-Version: 13" \
    http://10.249.64.103:8080

效果(curl 对回应消息只能拼接, 不换行):
server.png

JavaScript WebSocket API 文档

官方文档在这里: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket
这个 WebSocket 类很简单:

  1. 构造函数给出服务器的地址, 包括ws协议, host 加 port;
  2. 有几个字段: 比如 url, readyState, protocol, bufferedAcmount, binaryType等, 有些是只读的;
  3. 客户端只有发送 send() 和 close() 方法;
  4. event handler:

    1. message: 当收到消息时;
    2. open: 当连接建立时;
    3. error: 当发送错误时;
    4. close: 当关闭连接时;

Node.js 的 WebSocket 实现库 ws 的文档

官方文档: https://www.npmjs.com/package/ws#sending-and-receiving-text-data
它不仅仅包含一个server端的API实现, 还包含一个做为 Node.js 客户端端代码实现

WebSocket 协议

官方文档: https://www.rfc-editor.org/rfc/rfc6455
当我们看过上面的 WebSocket 的例子之后, 再来看这个RFC 文档, 就不是那么难了.

为什么需要 WebSocket 协议

在 web 通信等某些场景下, 如果服务端发生了某些事件, 需要实时推送给客户端. 在传统的基于web的技术下, 需要客户端不断的去poll消息, 不管服务端到底有没有事件更新, 每次poll 都需要客户端发送一个http request, 并且如果发送的频率过低, 可能不能及时收到服务端事件更新, 如果频率过高, 又会对网络和服务端造成一些压力.

所以, 如果建立一个连接的情况下, 服务端事件变更主动推送客户端, 客户端只要等待就好了, 就完美解决了这个问题, 于是就有了 WebSocket 协议.

WebSocket 基本介绍

  1. 它可以使客户端和服务端在一个连接里面双向不间断通信;
  2. 通信过程分2阶段, 先是使用http协议握手连接, 然后以数据帧的方式双向发送数据;
  3. 广泛使用在游戏, 股票等需要实时消息通讯等软件中;
  4. WebSocket 可以使用现有web的 proxy和认证等成熟的机制;
  5. 通常开在80或443, 可以通过防火墙;

通常的 连接握手协议:

The handshake from the client looks as follows:

        GET /chat HTTP/1.1
        Host: server.example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Origin: http://example.com
        Sec-WebSocket-Protocol: chat, superchat
        Sec-WebSocket-Version: 13

   The handshake from the server looks as follows:

        HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
        Sec-WebSocket-Protocol: chat