[吐槽] 新手劝退元凶: url.el #我们中出了一个叛徒

相信很多人都被 url.el 坑过而没有意识到,通常第一反应是:上代理、改 tls。然而未必奏效,因为敌在本能寺。

据我不缜密的观察,这种情况直到 26.3 (都快到 9102 年了) 才有所改善。

实际上 25.3 以及之前版本的代理是没法用的:

(with-emacs-25.3
  (require 'tls)
  (with-eval-after-load 'tls
    (push "/private/etc/ssl/cert.pem" gnutls-trustfiles))
  (setq tls-checktrust t)

  (let ((url-proxy-services
         '(("http"  . #1="127.0.0.1:8123")
           ("https" . #1#))))
    (with-current-buffer
        (url-retrieve-synchronously "https://gnu.org")
      (buffer-string))))
;; =>
;; "HTTP/1.1 400 Couldn't parse URL
;; Connection: close
;; Date: Sat, 23 Nov 2019 13:34:14 GMT
;; Content-Type: text/html
;; Content-Length: 453
;; Expires: 0
;; Cache-Control: no-cache
;; Pragma: no-cache
;;
;; <!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">
;; <html><head>
;; <title>Proxy error: 400 Couldn't parse URL.</title>
;; </head><body>
;; <h1>400 Couldn't parse URL</h1>
;; <p>The following error occurred while trying to access <strong>https://gnu.org</strong>:<br><br>
;; <strong>400 Couldn't parse URL</strong></p>
;; <hr>Generated Sat, 23 Nov 2019 21:34:14 CST by Polipo on <em>mbp33.lan:8123</em>.
;; </body></html>
;; "

这个 BUG 早在 2012 年就存在了,直到 26.1 发布的时候才修复:

https://debbugs.gnu.org/cgi/bugreport.cgi?bug=11788

GNU bug report logs - #11788 url-http does not properly handle https over proxy Previous Next

Package: emacs;

Reported by: Andreas Schwab <schwab linux-m68k.org>

Date: Tue, 26 Jun 2012 10:25:02 UTC

Owned by: Magnus Henoch <mange freemail.hu>

Severity: wishlist

Tags: fixed, patch

Merged with 10, 12636

Found in version 24.2.50

Fixed in version 26.1

Done: Lars Magne Ingebrigtsen <larsi gnus.org>

Bug is archived. No further changes may be made.

没想到 26.2 类似的问题又重现了:

(with-emacs-26.2
  ;; Fix 400 issue
  ;; (setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")
  (with-current-buffer
      (url-retrieve-synchronously "https://www.adafruit.com/feed/quotes.xml")
    (buffer-string)))
;; =>
;; "HTTP/1.1 400 Bad Request
;; Server: cloudflare
;; Date: Sat, 23 Nov 2019 09:32:25 GMT
;; Content-Type: text/html
;; Content-Length: 253
;; Connection: close
;; CF-RAY: -
;;
;; <html>
;; <head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
;; <body>
;; <center><h1>400 Bad Request</h1></center>
;; <center>The plain HTTP request was sent to HTTPS port</center>
;; <hr><center>cloudflare</center>
;; </body>
;; </html>
;; "

然后 26.3 修复了。接下来 27.1 会不会再出幺儿子,拭目以待。

1赞

为什么url.el底层不用libcurl?libcurl一直很稳定,难道是libcurl不够清真?

url.el 配合TLS用的时候是有问题的。参见

解决方案是手动设置TLS加密强度

(let ((gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3"))
         ...)

我的init.el就有这么一行,忘了什么时候看到的了。

「HTTPS 代理问题 #11788 」和「TLS 问题 #34341」没关系吧

HTTPS 代理问题是因为 url-proxy 代理的时候,没考虑代理 HTTPS 时需要用 HTTP Connect 方法。我前两天给 Emacs url.el 配置 PAC 的时候,发现 url-proxy 实际也不支持SOCKS 代理,url-find-proxy-for-url 有提到 SOCKS 代理,但是 url-proxy 却只支持 HTTP 代理。

至于 TLS 问题,我就一窍不通了,而且也几乎没困扰到我。

(新的)http协议挺复杂,最好是用现成的C库。自己实现太刚了。

不知道怎么想的,我看 request.el 就很好啊。

我前面例子有这一行,为了抛错特意注释掉了。

后端用gnutls很不方便,我配置文件里用url-retrieve下载一些文件。每次用新机器初始化emacs配置,通常默认不安装gnutls,代码走到url-retrieve就报错了

continuous improvement :slight_smile:

私以为底层还是用标准的 C/C++库,甚至 rust,可移植性强、性能高,上层只需要封装就好。

第三方的可以绑定一下libcurl,内置的url.el估计没救了,url.el也是用lisp实现了大部分逻辑,基于Emacs底层的network stream机制。和libcurl这种已经封装好高级逻辑的不同。

A live example :zipper_mouth_face:

1赞

在emacs里访问socks服务器有什么api可用吗?或者调用linux的命令行?

make-network-process 就可以吧?

可以,SOCKS 是 TCP 层协议,所以能建立 TCP Socket 就行,make-network-process 支持 UDP、TCP 和 Unix domain socket,不支持 Raw socket。

而且 Emacs 自带了一个 SOCKS 5 客户端 socks.el,但我刚刚试了试没成功,没搞明白什么情况,连 HTTP 也不行。而且它的代码质量也不高。

发现我把 socks-server 设置错了,改正后可用,下面用 socks.el 访问 example.com

;; $ ssh -D localhost
;; $ curl -x socks5://localhost:8080 -v -I example.com

(setq socks-server '("Default server" "0.0.0.0" 8080 5))
;; => ("Default server" "0.0.0.0" 8080 5)

(socks-open-network-stream "hello" "*hello*" "example.com" 80)
;; => #<process socks>

(process-send-string (get-process "socks")
                     "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
;; => nil

;; 打开 *hello* 查看 HTML

不清楚如何测试 HTTPS。

1赞

这就相当于:

(let ((socks-server '("Default server" "0.0.0.0" 8080 5))
      (url-gateway-method 'socks))
  (display-buffer
   (url-retrieve-synchronously "http://example.com")))

如果请求的是 HTTPS,应该也调用同样的函数:url-retrieve -> url-open-stream -> socks-open-network-stream

1赞

应该是的。

socks.el + url.el 的一个问题是 DNS 还是在用户电脑上完成的,而不通过 SOCKS(SOCKS 5 支持 UDP,DNS 使用 UDP),所以 DNS 有问题的话,网站照样访问不了。Mac 下,系统设置开启 SOCKS 代理之后,Chrome 和 Safari 的 DNS 也会走 SOCKS 代理,但 FireFox 貌似不会。

2赞

这段不必划掉,socks.el 的实现或许真的有问题,又或是在使用 socks-open-network-stream 之前有些前置的条件必须设置而文档没有说明。

之所以这样说,是因为我在一个运行了很久、跑了各种测试片段的 Emacs 中执行 (socks-open-network-stream "hello" "*hello*" "example.com" 80) 是成功的,但是开启一个全新的 Emacs 却始终失败,而且提示信息也未反映出失败的真正原因,可见这个失败是意料之外的情况。

以下代码中的断言在我电脑(macOS 10.12.6, Emacs 25.1~27.0)上 100% 成立:

;;; test-socks.el --- Test socks -*- lexical-binding: t; -*-

;; 2019-12-20 18.34.04

(toggle-debug-on-error)

(require 'cl-macs)
(require 'socks)

(let ((ver (string-to-number emacs-version)))
  (cl-assert (<= 25.1 ver) nil (format "Expected 25.1 or newer, actual %s" ver)))

(setq socks-server (list "V2Ray" "127.0.0.1" 7891 5))

(cl-assert (equal '(wrong-type-argument number-or-marker-p nil)
                  (condition-case err
                      (socks-open-network-stream "example" "*example*" "example.com" 80)
                    (error
                     err))))

(cl-assert (setq proc (url-open-stream "example" "*example*" "example.com" 80 'socks)))
(message "==> process status: %s" (process-status proc))

;; Local Variables:
;; quickrun-option-cmd-alist: ((:command . "/bin/bash")
;;                             (:exec    . ("emacs-nightly -Q -l %s --batch")))
;; End:

;;; test-socks.el ends here

socks-open-network-stream 失败的原因也许可以从更上层的 url-open-stream 中找到端倪。