11
6
2017
0

使用 Python 读取火狐的 cookies

这事本来是有个 browsercookie 库可以做的,但是初看它就有不少问题:

  1. 不能指定要使用的火狐 profile(后来发现可以指定数据库文件)。
  2. 找不到 sessionstore.js 文件时会向标准输出打印一行信息。对于 cron 脚本,这种行为是非常非常讨厌的。

我在尝试解决这些问题时,又发现了额外的问题:它每次都要把所有的 cookie 全部读取。再加上不必要地导入 keyring、Crypto 等库,让我想放弃了。

于是我考虑自己实现一个 cookiejar。但发现它有如下问题:

  • 公开接口和实现细节没有清晰地分离
  • 没有提供存储和读取 cookie 的抽象,而是存在一个字典里

这样扩展起来就十分令人不爽了,也不知道能正常工作多久。

也罢,cookiejar 是个十分复杂的东西,我不如实现一个获取匹配的 cookie 的独立功能,然后通过各种姿势传给 HTTP 客户端库好了。

火狐的 cookie 数据库文件「cookies.sqlite」里就一个「moz_cookies」表,其结构也挺简单的。但是,怎么做 cookie 的匹配呢?既然决定放弃 Python 自带的 cookiejar,那就不看它,直接看火狐的源码好了。

于是去 DXR 上搜索火狐的源码。没费多少力气就找到了相关的部分,然后跟着代码就能知道是怎么匹配的了:

  1. 通过祼域名查得候选 cookie
  2. 根据域名、路径和 secure 等属性来过滤 cookie
  3. 就这样,没有第三步了

祼域名使用 tldextract 库来做,其它属性的匹配算法直接看火狐的代码。虽然是不熟悉的 C++ 代码,但是写得很棒,很容易理解。

把自己需要的部分写成 Python,得一新模块——firefoxcookies。就一个方法,返回一个 cookie 的字典,用起来也很方便。比如在我的 requestsutils.RequestsBase 中,这么干就可以了:

class FireRequests(RequestsBase):
  def initialize(self):
    self._fc = FirefoxCookies(os.path.expanduser(
      '~/.mozilla/firefox/nightly/cookies.sqlite'))

  def request(self, url, method=None, *args, **kwargs):
    if self.baseurl:
      url = urljoin(self.baseurl, url)

    cookies = self._fc.get_cookies(url)

    return super().request(url, method=method, cookies=cookies)

就这样就满足我的需求了。以后再有别的需求,再慢慢扩展。

Category: python | Tags: 火狐 python http
9
10
2016
8

如何取消对 WoSign 根证书的信任

WoSign 最近曝出一大堆问题,而且其处理问题的态度、解决问题的方式十分令人担忧。其官方形象也很糟糕,比如对国内 Let's Encrypt 用户进行 FUD 式威胁,比如只吊销了因 bug 误发的 GitHub 域名的证书,给某大学误发的证书视若无睹。具体问题有兴趣的可以去相关邮件组查看讨论。

这次问题我认为比起 CNNIC 要严重多了(最主要是这态度、这水准,就算它不主动作恶,也很容易被利用的样子),所以我获知情况之后就取消对了 WoSign 的信任。StartCom 签名 WoSign 的证书,所以需要一并吊销(反正也是一家人)。

火狐(桌面版)

依次打开「首选项」->「高级」->「证书」->「查看证书」,找到并选择 StartCom 和 WoSign 下的所有证书(使用 Shift 键可以选择连续的项目),然后点「编辑信任」按钮,取消弹出框中三个选项框的勾选。

Arch Linux

archlinuxcn 源用户直接执行命令:

sudo pacman -Sy revoke-disputable-ca

手动操作的话,是这样子。把需要取消信任的证书复制(不要软链接)到 /etc/ca-certificates/trust-source/blacklist/ 目录下,然后执行 update-ca-trust 命令即可。

我那个包里取消信任的证书是下边这八个:

CA_WoSign_ECC_Root.pem                    CNNIC_ROOT.pem                          StartCom_Certification_Authority_G2.pem  WoSign_China.pem
Certification_Authority_of_WoSign_G2.pem  StartCom_Certification_Authority.1.pem  StartCom_Certification_Authority.pem     WoSign.pem

Android

在「设置」->「安全」->「受信任的凭据」中禁用掉相关证书。这对 Opera Mobile 有效,但是对火狐无效。

目前还没找到火狐 Android 版禁用根证书的方式。

确认方法

访问 https://www.wosign.com/ 即可。

目前 USTC 已经更改证书,禁用这些根证书不影响 USTC 镜像源的使用。现在我因此不能访问的网站主要是 Python 邮件列表

Category: 网络 | Tags: http ssl 安全
5
30
2015
7

利用 mitmproxy 保存网页中的所有图片

有个需求,保存一个网页里的所有图片。

看上去是件简单的事情,拿火狐DownThemAll 扩展下载不就好了么。

然后发现那个网页仅限移动版访问。好吧,装个 UserAgent Switcher。然后发现它是通过 JavaScript 检测 UA 的,而 UserAgent Switcher 只改了 HTTP 头里的 UA。好吧,换个 muzuiget 的 User Agent Overrider。然后发现那些图片是动态加载的,DownThemAll 根本看不到地址。后来知道「查看网页信息」的「媒体」选项卡里也是可以保存图片的,不过那里显示的图片也不全……

于是我怒了,放弃继续尝试不同的工具,决定用程序员的方式来解决问题。

我管你怎么加载的,你总归是要从网络上下载图片不是么?那我就拿个代理把你访问过的所有图片全部保存下来好了 :-)

打开 mitmproxy 文档页,发现并没有现成的保存文件的功能。但是没关系,可以写脚本。看看示例,迅速写了以下不到二十行代码:

#!/usr/bin/mitmdump -s

from __future__ import print_function

import os
from urlparse import urlsplit

from libmproxy.protocol.http import decoded

def response(context, flow):
  with decoded(flow.response):
    if flow.response.headers['Content-Type'][0].startswith('image/'):
      url = urlsplit(flow.request.url)
      name = os.path.basename(url.path)
      with open(name, 'wb') as f:
        f.write(flow.response.content)
      print(name, 'written')

当然这是最终结果。不过和初版差别不大,毕竟就这么点儿代码。思路也很简单,凡是经过代理的图片都存起来。有点粗暴,但是好用。

代理脚本跑起来。然后启动一个全新的 Google Chrome,一个没有任何缓存存在的实例:

google-chrome-stable --proxy-server=http://localhost:8080 --user-data-dir=new

访问目标页面,启用移动版模拟并刷新,就可以看到各种图片都被保存下来了~~

Category: 网络 | Tags: mitmproxy HTTP 下载 代理 网络
11
21
2013
8

虾米歌词下载、Python Requests 库,以及 HTTP Keep-Alive

Requests

这是我第二次用 Requests 了。上一次是个下小说的脚本。我已经不记得自己为什么路过了 httplib2,也路过了 urllib3,却最终买了 Requests 的账。也许是不喜欢 httplib2 那个 Google Code 的首页,也许是厌倦了 urllib* 这种名字。不过我想更多的是开门见山的首页,以及开篇那段让人无法拒绝的介绍:

Requests is an Apache2 Licensed HTTP library, written in Python, for human beings.

Python’s standard urllib2 module provides most of the HTTP capabilities you need, but the API is thoroughly broken. It was built for a different time — and a different web. It requires an enormous amount of work (even method overrides) to perform the simplest of tasks.

Things shouldn’t be this way. Not in Python.

相比之下,中文翻译太差了(而且已经陈旧了)。我在这里再译一个版本:

Requests 是使用 Apache2 许可证的 HTTP 库。用 Python 编写,为人类编写。

Python 标准库中的 urllib2 模块提供了你所需要的大多数 HTTP 功能,但是它的 API 烂出翔来了。它是为另一个时代、另一个互联网所创建的。它需要巨量的工作,甚至包括各种方法覆盖,来完成最简单的任务。

事情不应该是那样的,在 Python 世界里。

Requests 使用的是 urllib3,因此继承了它的所有特性。Requests 支持 HTTP 连接保持和连接池,支持使用 cookie 保持会话,支持文件上传,支持自动确定响应内容的编码,支持国际化的 URL 和 POST 数据自动编码。现代、国际化、人性化。相见恨晚

Keep-Alive

以前一直以为 Keep-Alive(连接保持)就是节省了 TCP 连接建立的时间。直到渐渐了解了 TCP 慢启动。直到自己偶然间亲自对比了一次。

使用 httrack 默认参数下载 PostgreSQL 9.3 文档,一千多个页面,29 分钟。后来才注意到 httrack 的 Keep-Alive 支持要写参数手动启用。

使用 wget,默认会使用 Keep-Alive 来复用已有连接。几乎同样的页面,只花了 12 分钟

urllib3 说 它使用 Keep-Alive 单连接从 Google 下载 15 个页面比使用 urllib 每次建立新连接快了一倍。我这里的结果比它的测试还要好呢。

也许,支持 Keep-Alive,是我的 nvchecker 使用 pycurl 要快很多的很重要的一个原因吧。

其它

可惜的是,Requests 不支持 Tornado 的异步调度框架。不过还好,那边我可以用 libcurl,虽然 API 不 Pythonic,至少 cookie 管理和连接管理都很健全。

对了,虾米歌词下载脚本还是在老地方,歌词的获取参考了 you-get 的代码。Requests 的作者 Kenneth Reitz 也有一些其它有意思的东西,包括之前我发现但没意识到是他做的的、用于 HTTP 客户端测试的 Httpbin 网站。

Category: python | Tags: python tcp http
10
26
2012
4

在 Python 中流式解压 gzip 数据

在处理 HTTP 响应时,我需要来一段数据就处理一段。为了节约网络资源,我开启了 gzip 传输。然后问题来了:有什么办法把 gzip 过的数据一段段传进去,它能一段段地解压并吐出数据呢?gzip 模块虽然支持fileobj参数,但是它读不到数据时认为数据流已经结束,然后进行 CRC 校验……这里有个人也这样尝试过。解决办法也有了

d = zlib.decompressobj(16+zlib.MAX_WBITS)

使用一个神奇的数字构造一个decompress对象,然后不断地调用它的decompress方法就可以得到一段段的解压数据了 :-)

然后,我发现我真的想太多了——我用的可流式解析 HTTP 响应的库 http-parser 原生支持解压的,而且同时支持 gzip 和 deflate 方法!这个库能很好地适配到异步 I/O 框架中,可是文档太少了,这个解压的支持 docstring 里都没写,看了源码才知道 :-(

Category: python | Tags: Python HTTP
10
25
2012
24

使用 TLS/SSL 加密你的 HTTP 代理

HTTP 代理是明文的,这导致实际访问的 URL 可以被他人监测到。即使使用 HTTPS 协议,经过 HTTP 代理时会发送CONNECT请求,告诉代理要连续到远程主机的指定端口。于是,访问的目标域名暴露了。

有没有办法将传输内容加密呢?比如像 HTTPS 那样,使用 TLS 协议连接到代理服务器,然后再进行 HTTP 请求。很遗憾的是,我在 ziproxy 的配置里没有发现这样的选项。在 shlug 邮件列表里询问后,Shell Xu 提到了 stunnel 这个工具。以前我试过用它把 HTTP 的网站转成 HTTPS 的,但是网站后端程序无法知晓用户实际上使用的是 HTTPS,有些郁闷,就没管它了。

这次再次请出 stunnel,在代理服务器上执行如下命令:

sudo stunnel -d 0.0.0.0:8081 -r localhost:8080 -p /etc/stunnel/stunnel.pem

这样,所有到服务器的 8081 端口的请求,都会经过 TLS 解密后传递给 8080 端口。同时响应的数据也会被加密后再返回请求方。

接下来的问题是,浏览器无法直接使用这种代理。实际上除了拿 openssl 命令手动连接外,我不知道任何程序能够使用这种代理。那好,本地弄个反过来加密/解密的服务好了。还是使用 stunnel。不过出了点意外:Arch Linux 的 stunnel 是第四版,不再用命令行参数,转而使用配置文件了。于是参考这篇 Upgrading to stunnel 4,写了份 stunnel4 的配置文件:

compression = zlib
foreground = yes
output = /dev/stdout
client = yes
pid = /tmp/stunnel.pid
# or will output to syslog :-(
output = /tmp/stunnel.log

[name]
accept = 8082
connect = server.com:8081

这样在本地 8082 端口监听,把所有请求加密后转发到 server.com 的 8081 端口。同时响应的数据会被解密后再返回。

现在,所有与代理服务器传输的数据都被加密了,不怕被偷窥啦。

后记:

后来,我发现其实代理服务器和我本机都装了两个版本的 stunnel,只是名字中不带版本号的一个是第三版而另一个是第四版而已……

再后来,我猛然想起神器 socat——这家伙是支持 OpenSSL 的!比如客户端这边像下边这样子就可以了:

$ socat tcp-listen:8082,fork openssl:server.com:8081,verify=0

socat 真是神器啊,cat、netcat、rinetd、stunnel 的功能都覆盖了!

这样使用的时候,每次来新请求时,socat 会 fork 一个新进程来处理。有点浪费资源。不过略微查看了下,stunnel 似乎也一样。

4
3
2012
6

Tornado 与文件上传

Tornado 自身是不支持大文件上传的。对于接收到的文件,它会把文件内容保存在内存里,而不是像 PHP 那样保存在临时文件里。这样对于大文件,问题很明显了——内容不够。所以,Tornado 上传文件的大小限制在 100M 以下了。Tornado 官方建议使用 nginx 的上传模块来处理文件上传。但是,我这个服务连 nginx 都没用的,不想为了这个还专门跑个 nginx。

于是,我尝试性地写了这么几百行代码。POST 上传的数据是multipart/form-data格式的,没有找到对应的 RFC,就对照着 HttpFox 显示的实际上传数据和 tornado 已有的代码进行修改。我理解的multipart/form-data格式是这样子的:

首先,在请求头里指定Content-Type: multipart/form-data; boundary=---------------------------12724806401896502337880080173,其中 boundary 的值是浏览器生成的,它用来分隔上传的不同文件。请求体一开始便是添加了--前缀的这个 boundary。刚开始我没太注意前边的横线多了两个,造成接收到的数据不对。在之后是\r\n,然后是和请求头格式一致的信息,如:

Content-Disposition: form-data; name="file"; filename="name.txt"
Content-Type: application/octet-stream

Content-Disposition中指明了文件对应表单的域名以及上传的文件名。文件名的编码看来没有定论,我的火狐用的是 UTF-8 编码。这些信息之后又是\r\n\r\n,然后是文件内容。还好这文件内容没有经过任何编码,直接保存即可。完了之后,如果还有下一个域的数据,那么在一个\r\n后就是类似的格式,否则在\r\n后是带--前缀和--后缀的 boundary。Tornado 的代码暗示数据结尾的\r\n是可选的。

整个格式是这样子的:

-----------------------------12724806401896502337880080173
Content-Disposition: form-data; name="file"; filename="name.txt""
Content-Type: application/octet-stream

This is file content.

-----------------------------12724806401896502337880080173
Content-Disposition: form-data; name="file"; filename="c"
Content-Type: text/plain

Another file content.
-----------------------------12724806401896502337880080173--

所以,要把数据保存到临时文件里去,不需要担心怎么进行流式解码了,只要确定了文件数据的起始和结束就好。为了做到这个,我只好每次都将读到的数据的最后一段长度为带前缀的 boundary 的长度加一的部分保存下来与下次读到的数据合并再处理,以此保存每段数据都是检查过 boundary 的。再加上一是为了防止\r\n被打断,下次找到 boundary 后取它前边的数据时出错。这个 edge case 还是今天写这文章时才想到,又花了不少时间测试。

最后记下 md5sum 的用法。计算 md5 时,把输出重定向到文件,校验时直接md5sum -c md5文件就可以了,不需要人工对比。

又,netcat 很好用。Arch 下使用 OpenBSD 版 netcat 发送 HTTP 请求的命令是:

nc.openbsd -q0 localhost 4322 < post

Ubuntu 现在默认的 netcat 就是 OpenBSD 版,所以直接用nc命令就可以了。

Category: python | Tags: http python tornado

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com