分类 默认分类 下的文章

chrome 插件 SwitchyOmega 突然不能用 ERR_MANDATORY_PROXY_CONFIGURATION_FAILED

今天早上到公司, 任何网站都打不开了, 本地起的服务器本地端口都打不开. 出现如下页面:
not.png

症状

  1. 如果不用 SwitchyOmega 插件, 网站是可以打开的.
  2. 如果单单使用代理能打开墙外网站.
  3. 如果使用 Auto Switch 全都打不开.

我有一个墙外 proxy, 一个公司 PAC script. 然后使用前面2个组装一个 Auto Switch.

分析

根据症状分析 Auto Switch 里面的 公司 PAC script 出问题了. 于是检查 PAC script. 如下图:
php.png

竟然是一个 php 脚本, 这是???

说明公司的 PAC 脚本的服务器应该是 php 的, 它现在直接源文件返回了. 如果查看里面的php 内容, 发现它其实是产生 PAC 的脚本.

最后发现原来是公司这个 PAC 的服务器间歇性出问题, 有时候返回真正的 PAC, 有时候返回 php.

由 Transfer-Encoding chunked 引起的 site issue

Transfer-Encoding: chunked 介绍

Transfer-Encoding 是 HTTP 1.x 版本的一个header, 设置 payload 传输时候的一种编码. 可能的编码格式有: chunked, compress, deflate, gzip. 可以同时设置多个兼容的值. 这个header 只适用于 hop to top, 不适用于整个连接. 如果你想在整个连接上使用压缩算法, 应该使用 Content-Encoding header.

使用 Transfer-Encoding 的例子:

Transfer-Encoding: gzip, chunked

为什么要使用 chunked

假如一开始就知道要传输多长的payload 数据, 就可以使用 Content-Length header.
chunked: 一般因为一开始并不知道要传输多长的payload数据, 所以要一块一块传输, 在每一块的头上标注这一块有多长.
例子:

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

7\r\n
Mozilla\r\n
11\r\n
Developer Network\r\n
0\r\n
\r\n

payload header

HTTP 1.x 的 header 分为

由 Transfer-Encoding chunked 引起的 site issue

HTTP GET 请求

http GET 请求是最简单的请求类型. 在浏览器输入一个URL, 直接回车, 就是发送一个 http GET 请求. 一个简单的例子:

GET /path/to/resource?query=string HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, sdch
Connection: keep-alive

上面的最后一行header 之后, 会在发送一行只有\r\n 的行, 表示请求结束.

Transfer-Encoding: chunked 介绍

Transfer-Encoding 是 HTTP 1.x 版本的一个header, 设置 payload 传输时候的一种编码. 可能的编码格式有: chunked, compress, deflate, gzip. 可以同时设置多个兼容的值. 这个header 只适用于 hop to top, 不适用于整个连接. 如果你想在整个连接上使用压缩算法, 应该使用 Content-Encoding header.

使用 Transfer-Encoding 的例子:

Transfer-Encoding: gzip, chunked

为什么要使用 chunked

假如一开始就知道要传输多长的payload 数据, 就可以使用 Content-Length header.
chunked: 一般因为一开始并不知道要传输多长的payload数据, 所以要一块一块传输, 在每一块的头上标注这一块有多长.
例子:

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

7\r\n
Mozilla\r\n
11\r\n
Developer Network\r\n
0\r\n
\r\n

当 HTTP GET 遇到 Transfer-Encoding: chunked

根据上面的介绍, HTTP GET 请求不应该包含 payload. 所以如果在header 里面误发了Transfer-Encoding: chunked,会发生什么事情呢?

不同的服务器可能有不同的处理方式, 有的快速返回, 有的等待接受payload.

Tomcat 的处理方式

根据作者本地 debug 的实践, 到现在为止(20240713), Tomcat 的最新版本仍然是等待接收 chunked payload, 直到 socket read timeout.

这是某个Tomcat 版本等待读取 payload 的栈:

java.lang.Object.wait(Native Method)
org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.fillReadBuffer(NioEndpoint.java:1333)
org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.read(NioEndpoint.java:1234)
org.apache.coyote.http11.Http11InputBuffer.fill(Http11InputBuffer.java:785)
org.apache.coyote.http11.Http11InputBuffer.access$400(Http11InputBuffer.java:41)
org.apache.coyote.http11.Http11InputBuffer$SocketInputBuffer.doRead(Http11InputBuffer.java:1185)
org.apache.coyote.http11.filters.ChunkedInputFilter.readBytes(ChunkedInputFilter.java:310)
org.apache.coyote.http11.filters.ChunkedInputFilter.parseChunkHeader(ChunkedInputFilter.java:338)
org.apache.coyote.http11.filters.ChunkedInputFilter.doRead(ChunkedInputFilter.java:164)
org.apache.coyote.http11.filters.ChunkedInputFilter.end(ChunkedInputFilter.java:229)
org.apache.coyote.http11.Http11InputBuffer.endRequest(Http11InputBuffer.java:644)
org.apache.coyote.http11.Http11Processor.endRequest(Http11Processor.java:1184)
org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:430)
org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:926)
org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1791)
org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
java.lang.Thread.run(Thread.java:750) 

Tomcat 的逻辑是先逐行读取 header 信息, 直到读到 \r\n 行, 然后根据header设置需要的 InputFilter 列表(虽然是列表, 可能只有一个). 常见的 InputFilter 有:

  1. VoidInputFilter - 当 GET, HEAD 请求时用.
  2. ChunkedInputFilter - 当 chunked 的时候用.

一个请求的例子

下面是使用 python 写的一个发送 GET 请求并且设置 Transfer-Encoding: chunked 的例子:

import socket
from concurrent.futures import ThreadPoolExecutor
 
# Configuration
host = 'www.tianxiaohui.com'
port = 80
buffer_size = 4096
read_timeout = 100000  # Set read timeout to 10 seconds
 
def call():
    # Create a socket object using IPv4 and TCP protocols
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
    # Set the read timeout on the socket
    client_socket.settimeout(read_timeout)
 
    try:
        # Connect to the server
        client_socket.connect((host, port))
 
        # Prepare the HTTP request data
        http_request = ("GET /sell/marketing/v1/ad_campaign?limit=100&offset=0 HTTP/1.1\r\n"
                        f"Host: {host}\r\n"
                        "accept: application/json, text/json, text/x-json, text/javascript\r\n"
                        "accept-encoding: application/gzip, deflate\r\n"
                        "Transfer-Encoding: chunked\r\n"
                        "\r\n")
 
        # Send the HTTP request to the server
        client_socket.sendall(http_request.encode())
 
        # Receive the response from the server
        response = ''
        while True:
            part = client_socket.recv(buffer_size).decode()
            if not part:
                break
            response += part
 
    except socket.timeout:
        print("Read timed out")
        response = None
    finally:
        # Close the socket
        client_socket.close()
 
    # Return the response
    return response
 
# Number of parallel calls
num_calls = 1
 
# Use ThreadPoolExecutor to execute the calls in parallel
with ThreadPoolExecutor(max_workers=num_calls) as executor:
    # Submit all calls to the executor
    future_calls = [executor.submit(call) for _ in range(num_calls)]
     
    # Wait for all futures to complete and print their results
    for future in future_calls:
        response = future.result()
        if response is not None:
            print("Response:")
            print(response)

如果改成对着本地的 tomcat 调用, 可以看到它等在那里20ms, 这20ms 就是读取完 header 之后, 等待读取 chunked payload, 却迟迟等不来的结果, 最后只有等到 read timeout.

这是在最新的 Tomcat 10.1.25 上得到的栈:

java.lang.Thread.State: TIMED_WAITING (on object monitor)
    at java.lang.Object.wait([email protected]/Native Method)
    - waiting on <0x000000061a3aea90> (a java.util.concurrent.Semaphore)
    at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.fillReadBuffer(NioEndpoint.java:1280)
    - locked <0x000000061a3aea90> (a java.util.concurrent.Semaphore)
    at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.read(NioEndpoint.java:1181)
    at org.apache.coyote.http11.Http11InputBuffer.fill(Http11InputBuffer.java:789)
    at org.apache.coyote.http11.Http11InputBuffer$SocketInputBuffer.doRead(Http11InputBuffer.java:1195)
    at org.apache.coyote.http11.filters.ChunkedInputFilter.readBytes(ChunkedInputFilter.java:254)
    at org.apache.coyote.http11.filters.ChunkedInputFilter.fill(ChunkedInputFilter.java:295)
    at org.apache.coyote.http11.filters.ChunkedInputFilter.parseChunkHeader(ChunkedInputFilter.java:328)
    at org.apache.coyote.http11.filters.ChunkedInputFilter.doRead(ChunkedInputFilter.java:136)
    at org.apache.coyote.http11.filters.ChunkedInputFilter.end(ChunkedInputFilter.java:181)
    at org.apache.coyote.http11.Http11InputBuffer.endRequest(Http11InputBuffer.java:646)
    at org.apache.coyote.http11.Http11Processor.endRequest(Http11Processor.java:1188)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:429)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:904)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
    at java.lang.Thread.run([email protected]/Thread.java:833)

由http header Keep-Alive 引出的问题

由于建立一个网络连接相对于本地操作十分昂贵, 所以系统应该尽量重用一个已经建立的连接.

因此, 在http 1.0的规范中定义了 Keep-Alive header, 一个典型的例子:

Keep-Alive: timeout=5, max=1000

这里的 timeout 表示如果没有后续的重用, 让这个连接最短逗留(idle) 5秒才能关闭它. 若有重用, 这从最后一个重用重新开始计时.
这里的 max 表示如果这个连接被一直重用, 但是这个连接最多只能发送1000个请求(或响应), 一旦超过这个数目, 即使能还没等idle 5秒, 也要关闭它. 所以它表示最多可以通过这个连接发送多少请求(或响应).

http 1.1

http 1.1 默认是持久化连接, 所以默认暗含是 Keep-Alive 的, 即便不发送这个头字段. 如果不在 http 的请求和响应中发送这个头字段, 那么连接的两端又该如何控制 idle 的timeout 和 一个连接最多可以发送的请求(或响应)的数目呢?

既然是为了重用, 那就最大化重用. 是不是一直重用某个连接, 一直不关闭就好了呢?

timeout 的合理性

  1. 从资源管理的角度. 若服务器端一直保持连接, 那么每个新用户都保持一个连接, 即便用户几分钟没数据传输, 这会导致服务器资源(连接, 内存等)很快被耗尽. 若客户端程序(浏览器, 代码客户端)也会有一样的问题.
  2. 从公平的角度. 若一个连接一直保持, 那么后来的新用户肯定无法再连接上.
  3. 从安全和性能的角度. 若保持很多没有数据的连接, 可能会有潜在的性能降级或者内存泄漏问题.

设置一个合理的 timeout 值, 保证一旦一段时间没数据传输, 就关闭这个连接.

max 请求的合理性

既然有了 timeout 可以保证 idle 了就关闭, 同时为了最大化重用, 那么是不是不需要 max 了呢?
考虑一种情况: timeout 是5秒, 但是每当 idle 4秒的时候, 就有一个新的请求(或响应), 那么这个连接将一直会被重用. 那么一直重用又有什么不好呢?

在理想的情况一下, 一直有数据固定频率的传输, 永久的使用这个连接其实是非常美好的一种状态.

但是 max 也有其它合理的地方:

  1. 避免某些写的不好的代码把某些请求(request)资源绑定在这个连接之上, 导致内存泄漏, 若关掉连接则全部释放.
  2. 公平性. 比如某些抢票的网站, 避免某些人一直先保持一个连接, 导致后面的人根本连不上. max 保证多少次请求之后, 一定关闭.
  3. 对于某些负载均衡器(Loader Balancer), 如果某个特定IP上一直保持特别多的连接, 会导致不再均衡.

服务端的 timeout 和 max

虽然 http 1.1 不再设置 Keep-Alive 头字段, 默认是持久连接. 但是连接的2端仍然要对合理的这2种机制做处理. 下面列举一些常见的服务端的处理和配置.

Tomcat web 服务器

Tomcat 是一个常用的 Java web 服务器. 在今天(20240605)最新的 10.1 版本里面的 http Connector 的配置里, 就能看到关于 timeout 和 max 的配置:
keepAliveTimeout: 默认60秒.
maxKeepAliveRequests : 默认100.

nginx 服务器

nginx 的配置分别是:
keepalive_timeout: 默认75秒.
keepalive_requests: 默认 1000.
不过, nginx 还有一个 keepalive_time: 类似max, 它从时间角度约束一个连接从开始建立最长存活多久, 区别于idle timeout.

envoy proxy

对于 Envoy proxy 没有对应的完全一致的概念, 不过它有另外2个参数:
common_http_protocol_options.idle_timeout:
stream_idle_timeout:
更多内容参考: https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts
https://github.com/envoyproxy/envoy/issues/8652

客户端的 timeout 和 max

既然对于 http 1.1 默认是持久的, 那么客户端也是暗含的, 不发送 Keep-Alive 的, 那么客户端是怎么处理的呢?

chrome 浏览器

没有找到相关文档.

Apache httpClient

在 4.5 版本 2.6 节 连接管理部份(https://hc.apache.org/httpcomponents-client-4.5.x/current/tutorial/html/connmgmt.html), 有下面一段描述:
“If the Keep-Alive header is not present in the response, HttpClient assumes the connection can be kept alive indefinitely”

JDK HttpClient

对于取值的大概处理:
https://github.com/openjdk/jdk/blob/e1870d360e05c372e672b519d7de2a60c333675b/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L892-L937

max 的取值:

https://github.com/openjdk/jdk/blob/e1870d360e05c372e672b519d7de2a60c333675b/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L902C25-L902C82

keepAliveConnections = p.findInt("max", usingProxy?50:5);

timeout 的取值

https://github.com/openjdk/jdk/blob/e1870d360e05c372e672b519d7de2a60c333675b/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L906C37-L918

    /*
     * The timeout if specified by the server. Following values possible
     *  0: the server specified no keep alive headers
     * -1: the server provided "Connection: keep-alive" but did not specify a
     *     a particular time in a "Keep-Alive:" headers
     * -2: the server provided "Connection: keep-alive" and timeout=0
     * Positive values are the number of seconds specified by the server
     * in a "Keep-Alive" header
     */
OptionalInt timeout = p.findInt("timeout");
if (timeout.isEmpty()) {
    keepAliveTimeout = -1;
} else {
    keepAliveTimeout = timeout.getAsInt();
    if (keepAliveTimeout < 0) {
        // if the server specified a negative (invalid) value
        // then we set to -1, which is equivalent to no value
        keepAliveTimeout = -1;
    } else if (keepAliveTimeout == 0) {
        // handled specially to mean close connection immediately
        keepAliveTimeout = -2;
    }
}                          

max 用完之后, 关闭连接:

https://github.com/openjdk/jdk/blob/e1870d360e05c372e672b519d7de2a60c333675b/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L443-L453

客户端总结

客户端也会考虑 连接idle 的timeout 和 max 请求数, 但是很多情况下, 它没有服务端那么紧迫, 但是客户端也有这些机制, 只是没有那么透明.

urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate

最近一个项目把 python 从 3.9 升级到3.11, 于是把 3.9 删除, 使用 brew install [email protected], 其它都正常, 就是使用 requests 访问各个https 地址的时候, 就报错:

Failed to send a request to Slack API server: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)>

因为出错的是访问 slack api(其实跟访问那个url没关系), 所以找到了这个帖子: https://github.com/slackapi/bolt-python/issues/673.

可是里面提到的 Certificates.command 对于我的安装路径根本就不存在. 于是怀疑 brew 安装有问题, 重新安装, 没看到任何错误, 运行程序还少一样的错.

最终根据一篇帖子的描述, 找到了答案.

启动python, 运行下面的脚本:

supra@host tools % python -V
Python 3.11.9
supra@host tools % python
Python 3.11.9 (main, Apr  2 2024, 08:25:04) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import _ssl; _ssl.get_default_verify_paths()
('SSL_CERT_FILE', '/usr/local/etc/openssl@3/cert.pem', 'SSL_CERT_DIR', '/usr/local/etc/openssl@3/certs')

可以看到它用的证书在/usr/local/etc/openssl@3/cert.pem, 发现这个是一个软链接, 链接到 ../ca-certificates/cert.pem, 可是这个文件夹都不存在.

于是新建这个文件夹, 并且把正确的证书放到那里, 然后就工作了.

如果你仅仅是想安装某个模块, 可以先忽略这个证书,如果你真的信任源的话:

pip install --trusted-host mirrors.tuna.tsinghua.edu.cn   -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple transformers