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 似乎也一样。

10
12
2012
18

wget 下载云诺网盘文件

今天下载别人在云诺网盘上分享的文件,无奈网络太差,火狐才下了不到百分之一就认为下载完成了。每当这个时候,我便会祭出「中国特色网络」下的下载利器——wget。每当浏览器下不好文件时,wget 总会不屈不挠地一次次坚持,直到文件真正下载完成。

不过,这次对付云诺这个互联网友好发展的阻碍者时出现了问题。wget 总是下载到那个网页,即使指定 UserAgent 或者 Cookie 也没用。后来经过我不懈地尝试,发现指定Referer即可,例如:

wget --header 'Referer: http://s.yunio.com/xMliei' http://s.yunio.com/public/download/token/xMliei

另外,要取得正确的文件名,需要使用--content-disposition选项。不过,可能还需要使用ascii2uni -a J来处理百分号转义。下边是我的~/.wgetrc

# 不要乱转义中文
--restrict-file-names=nocontrol
# 使用重定向后的文件名
--trust-server-names=on
--content-disposition=on
Category: 网络 | Tags: wget 网络
8
28
2012
10

UDP打洞实验

两台没有外网 IP、在 NAT 后边的主机如何直连?UDP打洞通常可行,但是需要第三方服务器。方法如下:

在服务器 S 上监听一个 UDP 端口,在收到 UDP 数据包后把源地址发回去。代码如下(github):

import sys
import time
import socket

def main(port):
  s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  s.bind(('', port))
  try:
    while True:
      data, addr = s.recvfrom(4096)
      back = 'Your address is %r\n' % (addr,)
      s.sendto(back.encode(), addr)
      print(time.strftime('%Y-%m-%d %H:%M:%S'), addr, 'just sent us a message:', data.decode('utf-8', 'replace'), end='')
  except KeyboardInterrupt:
    print()

if __name__ == '__main__':
  try:
    main(int(sys.argv[1]))
  except (ValueError, IndexError):
    sys.exit('which port to listen?')

主机 A 发送数据包:

$ socat readline udp:xmpp.vim-cn.com:2727,sourceport=4567
my addr?
Your address is ('a.b.c.d', 40060)

输入任意消息并回车,一个 UDP 就从本地的 4567 发送出去了。从上述示例我们可以看到,NAT 设备转发时是从 40060 端口发送出去的。为了让服务器返回的数据能够到达内网主机,在一段时间内,NAT 设备会记住外网来自 40060 端口的 UDP 数据包要发送给主机 a.b.c.d 的 4567 端口。完全圆锥型NAT不会在意外部数据包是从什么地方发回来的。受限圆锥型NAT会忽略掉其它主机的数据包,上例中只认可来自 xmpp.vim-cn.com 的数据包。端口受限圆锥型NAT更进一步地要求源端口(上例中是 2727)必须跟之前发出的数据包的目的端口一致。当然,「之前发出的数据包」不必是最后一个。所以,除了最后一种——对称NAT——之外,其它类型的NAT都是有可能成功穿透的。参见维基百科条目网络地址转换STUN

后来通过 pystun 程序,我得知我所处的 NAT 是完全圆锥型的。

在知道 A 的发送地址后,主机 B 就可以向这个地址发送数据了。接下来的操作使用 socat 命令就是:

# host A
$ socat readline udp-listen:4567
# host B
$ socat readline udp:A:4567

然后 B 先发送数据让 A 知道 B 的地址(socat 会 connect 到这个地址),双方就可以相互通信了。当然,因为是 UDP 协议,所以通信是不可靠的,丢包啊乱序啊都有可能。

2013年10月13日更新:想要连接到 NAT 后边的 mosh 请看这里

Category: 网络 | Tags: python 网络 socat UDP
7
27
2012
8

fcitx-remote 接口通过 socat 跨主机使用

在使用 Mac OS X 时,我十分想念 fcitx.vim 插件在使用 Vim 时能智能切换输入法的激活状态。所以我换回 Arch Linux 了。关于 Mac OS X 与我的「不兼容」还是留到下次再说,这次解决的问题是,当我 ssh 到另一主机上使用 Vim 时,如何让 fcitx.vim 能够控制本机的输入法状态?

fcitx-remote 接口使用的是 UNIX 套接字文件,因此天生是不能跨主机通信的(因此不用担心局域网里其它人捣乱)。现在,为了进行跨主机通信,当然要使用网络套接字了。既然都是套接字,转发下就可以了嘛。于是想到 socat。

在远程机器监听一个套接字文件,转发到本地机器的 8989 端口:

socat UNIX-LISTEN:/tmp/fcitx-remote.sock,fork TCP:192.168.2.142:8989

在本地监听网络 8989 端口,转发到本地 fcitx 的套接字:

socat tcp-listen:8989,fork UNIX-CONNECT:/tmp/fcitx-socket-\\:0

fcitx.vim 使用更新后的 1.2 版,然后告诉它你要使用的套接字文件地址:

export FCITX_SOCKET=/tmp/fcitx-remote.sock                                                                                                                                               

然后就可以啦~

最后,贴一张测试过程中抓到的 htop 的图片,2 万多进程哦,htop 已经卡了,实际的 load 请看右下角的红色数字。我执行killall socat命令后等了几分钟,终于因为内存耗尽系统开始重新缓慢工作了。数次 killall 后终于恢复正常……再次测试前果断先ulimit -u 1000 :-)

Category: Linux | Tags: fcitx vim 网络 socat
6
6
2012
12

编程获取本机IPv4及IPv6地址

首先,我要通过编程直接获取,而不是去读诸如ifconfig等命令的输出。

其实是只想获取IPv6地址的,不过我猜想它们差不多,也确实看到不少相关搜索结果,于是顺带着看了。

首先,使用gethostbyname查自己通常是不行的,因为可能得到127.0.0.1,而且我猜,这样不能处理拥有多个IPv4地址的情况。另外一种方式是连上某个主机,然后调用getsockname。这样需要能够直接连上那个主机,好处是如果有多个网络接口,这样可以知道到底走的是哪个接口,调试网络时不错。我最满意的方案在这里,使用ioctl来获取。这个方法可以获取指定网络接口的IPv4地址。至于有哪些网络接口嘛,直接读/proc/net/dev吧。

import fcntl
import socket
import struct
ifname = b'eth0'
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 0x8915 是 SIOCGIFADDR
ip = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15]))[20:24])
print(ip)

然而,这样只能获取IPv4地址。创建个AF_INET6的 socket 传过去会报错「Inappropriate ioctl for device」。那怎么办呢?Google 没找到,我去搜了下内核源码。inet_ioctl里有对SIOCGIFADDR的处理。但是,inet6_ioctl里却没有了。

于是,我只好去下载ifconfig所属的 net-tools 的源码,找到相关代码:

#if HAVE_AFINET6
    /* FIXME: should be integrated into interface.c.   */

    if ((f = fopen(_PATH_PROCNET_IFINET6, "r")) != NULL) {
    while (fscanf(f, "%4s%4s%4s%4s%4s%4s%4s%4s %08x %02x %02x %02x %20s\n",
              addr6p[0], addr6p[1], addr6p[2], addr6p[3],
              addr6p[4], addr6p[5], addr6p[6], addr6p[7],
          &if_idx, &plen, &scope, &dad_status, devname) != EOF) {
        if (!strcmp(devname, ptr->name)) {
        sprintf(addr6, "%s:%s:%s:%s:%s:%s:%s:%s",
            addr6p[0], addr6p[1], addr6p[2], addr6p[3],
            addr6p[4], addr6p[5], addr6p[6], addr6p[7]);

这里就是ifconfig输出IPv6部分的代码了。可以看到它打开了一个奇怪的文件。跟过去,发现是

#define _PATH_PROCNET_IFINET6       "/proc/net/if_inet6"

囧,这个文件我早就发现过了的。看来和IPv4的情况不同,IPv6地址只能通过/proc里的文件获取了。而且输出成人可读格式不容易(ifconfig是自己实现的)。

PS: 我还发现了件好玩的事,在 Linux 源码的include/linux/sockios.h中,SIOCGIFINDEX中的字母 C 写漏了。通过git blame我发现,这个拼写错误在至少七年前 Linux 内核代码迁移到 git 前就修正了。Linus Torvalds 说之前的代码导入到 git 后有 3.2GB。我不得不承认这是个无比正确的决定,因为现在的.git已经有600多兆了,git 不支持断点续传,clone 下来已经很不容易了。

另外,我还联想到了 Unix 系统调用中的creat,以及 HTTP 协议中的referer :D

#define SIOCGIFINDEX    0x8933      /* name -> if_index mapping */
#define SIOGIFINDEX SIOCGIFINDEX    /* misprint compatibility :-)   */
Category: Linux | Tags: C代码 linux python 网络
3
29
2012
3

病毒 xviewer.exe 简析

搞到了个病毒,想看看它想干什么,扔虚拟机里跑了几次。PS: 这个病毒真大,9M 多了。

第一次,观察到其进程结构为一个 winloads.exe 进程下生成三个 xviewer.exe 工作进程。winloads.exe 无法被 Process Explorer 杀死,而 xviewer.exe 可以。工作进程总保持为三个。病毒运行过程中,耳机不时传来 Windows 页面刷新的声音,猜测这家伙在用浏览器。第二次使用 Process Monitor 观察的结果证实了这个猜测,xviewer.exe 在大量访问C:\Documents and Settings\LocalService\Local Settings\Temporary Internet Files\Content.IE5\index.dat文件。不过,Process Monitor 可能还是太弱了,不能看到事件的详细信息,也没看到网络访问。

第三次测试,将 vbox 的 NAT 网卡关掉,只留下一块 host only 的。然后本机开启 NAT 转发:

sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"
sudo iptables -t nat -A POSTROUTING -s 192.168.56.0/24 -j MASQUERADE

使用 Wireshark 在 vboxnet0 接口上监听。虚拟机上手动设置网关为192.168.56.1,DNS 服务器为我本机上的 dnsmasq,也就是相同的地址。

设定好后,再次运行病毒,可以看到其

  1. 首先访问 www.wandianji.com 上的列表文件(http://www.wandianji.com/xsoft/update/update_plug_v2.txt)来更新;
  2. 再使用 POST 请求访问,获取一格式不明的链接列表;
  3. 接着开始访问百度,搜索关键词,访问网页。

原来是刷搜索排名的。难怪百度的搜索结果那么差,国内这些东西估计有不小的贡献吧?

PS: 我是用 XueTr 来结束病毒的进程们的,这个软件不支持进程树显示,但是可以多选来同时杀掉多个进程。

病毒样本及抓包结果下载链接在此

Category: Windows | Tags: windows 网络 病毒分析
3
18
2012
4

使用 gnokii 读取 3G 网卡的短信

使用 gnokii 读取 3G 网卡短信的方法ArchWiki上有写。安装 gnokii 后复制配置文件并将自己添加到uucp用户组中:

cp /etc/gnokiirc ~/.config/gnokii/config
sudo gpasswd -a `whoami` uucp

然后修改下配置文件,主要是port = /dev/ttyUSB0model = AT这两处。用户组的修改要下次登录时才生效,或者使用newgrp命令来登录到uucp组。据说此命令在某些 shell 里是内建命令,不过在 bash 和 zsh 里没有,只能调用外部命令,所以会开启一个新的 shell。

newgrp uucp

现在就可以读短信了。接收新信息并存储,可使用命令

gnokii --smsreader

完事之后按Ctrl-C中断。要把短信读出来,gnokii 可以把短信存储为 mbox 格式。这是一种邮件格式,使用 mutt 即可阅读。-f后边的sms即是要保存的文件名。注意,gnokii 仍会将消息输出到终端。ArchWiki 上说的是使用 xgnokii GUI 程序来读取,但是我没有找到这个程序(只有 manpage)。

gnokii --getsms SM 0 end -f sms

对于中文短信,这样会导致乱码。所以我写了个 Python 脚本来处理。三件事:一是将内容编码标识为 UTF-8,二是把按字节截断的邮件主题(短信正文的前若干个字节)最后几个无效的编码替换掉,三是将邮件主题按标准进行编码。这些事 Python 处理起来挺容易的 ;-)

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import os
import sys
import email.header

for i in os.fdopen(sys.stdin.fileno(), encoding='utf-8', errors='replace'):
  if i.startswith('Subject: '):
    s = i[9:-1]
    print('Content-Type: text/plain; charset=utf-8')
    print('Subject:', email.header.Header(s, 'utf-8').encode())
  else:
    sys.stdout.write(i)
Category: Linux | Tags: LInux Python 网络 mutt
9
29
2011
4

Flash 游戏 Music Catch 2 hack 攻略

Music Catch 是 Reflexive 出品的一款音乐游戏,其PC版本收费,但有两个免费的Flash版本。版本一是一个非常有限的预览版,只内置了一首音乐;版本二不仅内置了更多音乐,并拥有更多选项,它还支持从URL获取外部音乐:

不过,这些选项要逐个解锁——即完成它们。好不容易全部解锁后,终于可以播放自己的歌曲了。搭建个HTTP服务器,却发现需要公网能访问才行,而且速度死慢死慢的——因为它是从自家的网站的一个代理获取歌曲文件的。这也是Flash跨域请求的限制。简单抓包下就可以知道该怎么对付了。

首先修改 hosts 文件,把那个代理劫持到自己机器上:

127.0.0.1       wgextras.gamecentersolution.com

然后是crossdomain.xml文件,把它放自己的根目录下。

<cross-domain-policy>
    <site-control permitted-cross-domain-policies="master-only"/>
    <allow-access-from domain="*"/>    
</cross-domain-policy>

最后是那个代理脚本了,路径是/proxy.php。我简单地用PHP写了个:

<?php
# 为 Music Catch 2 而写
# 使其可以直接从本地读取音乐文件
error_reporting(0);
if($_POST['MusicCatch']){
  $mp3 = file_get_contents($_GET["url"]);
  header("Content-Length: ".strlen($mp3));
  header("Last-Modified: ".gmdate("F d Y H:i:s")." GMT");
  header("Connection: close");
  header("Accept-Ranges: bytes");
  header("Content-Disposition: attachment; filename=\"song.mp3\"");
  header("Content-Type: audio/mpeg");
  echo $mp3;
}else{
  header("HTTP/1.1 400 Bad Request");
?>
<title>400 Bad Request</title>
<h1>400 Bad Request</h1>
<hr/>
<p>This page is intended for Music Catch 2.</p>
<?php
}
?>

就是这样。现在再从URL加载音乐时,只需要一两秒的时间了。

最后,Music Catch 2 SWF 文件下载

Category: 未分类 | Tags: flash 网络
12
15
2010
4

从UDP到解决DNS污染的脚本

最近做网络课的实验,涉及UDP协议。因为UDP协议比TCP用得少,所以我以前没试过创建几个UDP的socket。现在忽然有了兴致,就试了试。

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

socket创建好了,往哪里发消息呢?我想到了DNS。首先要有报文。没找到容易上手的Python库,就Google到了这个,DNS报文的格式。没功夫细细研究,先弄个报文测试下:

q = b'>:\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07twitter\x03com\x00\x00\x01\x00\x01'
s.sendto(q, ('8.8.8.8', 53))

\x07twitter\x03com\x00就是要查询的域名了,我想测试什么,明白人都看得出来了。接下来接收回答,接收了多次:

>>> '.'.join(str(int(i)) for i in s.recv(4096)[-4:])
'46.82.174.68'
>>> '.'.join(str(int(i)) for i in s.recv(4096)[-4:])
'128.121.146.100'

再接收的话就阻塞了。对于普通域名,当然是最多只会接收到一次的啦。如果报文出错的话(比如我不小心少写了最后的\x00\x01),是收不到回答的。

有人依据此现象写了个pydnsproxy 放在Google Code上。主页说“暂不公布方法”,但其实很简单。下面是我整理过的代码,转成了Python3的语法:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

from socketserver import *
from socket import *
import sys, os

'''
来源 http://code.google.com/p/pydnsproxy/
'''

DEF_LOCAL_HOST = '127.0.0.1'
DEF_REMOTE_SERVER = '8.8.8.8'
DEF_PORT = 5350
DEF_CONF_FILE = 'dnsserver.conf'
DEF_TIMEOUT = 0.4

gl_remote_server = None

class LocalDNSHandler(BaseRequestHandler):
  def setup(self):
    global gl_remote_server
    if not gl_remote_server:
      remote_server = DEF_REMOTE_SERVER
    else:
      remote_server = gl_remote_server
    self.dnsserver = (remote_server, 53)

  def handle(self):
    data, socket = self.request
    rspdata = self._getResponse(data)
    socket.sendto(rspdata, self.client_address)

  def _getResponse(self, data):
    "Send client's DNS request (data) to remote DNS server, and return its response."
    sock = socket(AF_INET, SOCK_DGRAM) # socket for the remote DNS server
    sock.sendto(data, self.dnsserver)
    sock.settimeout(5)
    while True:
      try:
        rspdata = sock.recv(4096)
        break
      except error as e:
        if e.errno != 11:
          raise
        else:
          print("Try again")
    # "delicious food" for GFW:
    while True:
      sock.settimeout(DEF_TIMEOUT)
      try:
        rspdata = sock.recv(4096)
        print("GFWed?")
      except timeout:
        break
      except error as e:
        if e.errno != 11:
          raise
        else:
          print("Trying again")
    return rspdata

class LocalDNSServer(ThreadingUDPServer):
  pass

def main():
  global gl_remote_server
  try:
    if hasattr(sys, 'frozen'):
      dir = os.path.dirname(sys.executable)
    else:
      dir = os.path.dirname(__file__)
    confFile = os.path.join(dir, DEF_CONF_FILE)
    f = open(confFile, 'r')
    dns = f.read().split('=')
    f.close()
    if len(dns) == 2:
      if dns[0].strip().lower() == 'dns':
        gl_remote_server = dns[1].strip()
      else:
        pass
  except:
    pass
  dnsserver = LocalDNSServer((DEF_LOCAL_HOST, DEF_PORT), LocalDNSHandler)
  dnsserver.serve_forever()

if __name__ == '__main__':
  main()

注意到和原程序不同的是,我捕获了错误号为11的socket.error异常。这个是EAGAIN,“资源临时不可用”,只会在设置了超时后出现。man文档recv(2)对此的解释是:

EAGAIN or EWOULDBLOCK
       The socket is marked  nonblocking  and  the  receive  operation
       would  block, or a receive timeout had been set and the timeout
       expired before data was received.  POSIX.1-2001  allows  either
       error  to be returned for this case, and does not require these
       constants to have the same value,  so  a  portable  application
       should check for both possibilities.

不知道是怎么回事,再次接收却又可以收到数据。也许正如其名,是期待调用者再次尝试吧。我越来越觉得,Python 的异常处理里应该有tryagain这样的语句了。

2012年11月10日更新:更好地摆脱 DNS 污染,请参考此文

Category: 网络 | Tags: DNS python 网络 UDP
10
21
2010
6

Python HTTP 请求时对重定向中的 cookie 的处理

首先说明一下,我使用的是 Python3 的 urllib,但 Python2.x 同理(使用 urllib2)。

想用脚本去登录一个网站。和很多网站一样,该网站使用 cookie 来保存会话信息。这个我以前是自己提取 response 中的 Set-Cookie 头来处理的。这次本想如法炮制,却发现没保存需要的 cookie,所以登录失败。

很郁闷地想了半天,最后出去 wireshark 抓包,终于发现原来重要的 cookie 在登录后的应答中,但这个应答是个 302 重定向,所以 urllib 默认的 opener (urllib.request.urlopen)直接就跟从这个重定向了,没有对 cookie 进行任何处理。

我首先想到的是,不要跟从重定向。我看到有个 HTTPRedirectHandler,但文档里没写它怎么用。郁闷……自己找到 request.py 文件看源代码,折腾了好久无果,遂想到 Google (早该想到了。。。)于是找到了 StackOverflow 上。有两个解决办法:要么不跟从重定向,要么弄个 HTTPCookieProcessor 保存 cookie 信息。看我自己的需求,当然选后者了。而且,那个回答问题的人也没有给出如何不让它跟从重定向(所给代码只是在重定向前对 cookie 进行处理而已)。

于是,我再一次地打开了 http.cookiejar 的文档,尝试弄明白这东西到底怎么用。当初折腾 cookie 的时候,没弄明白这个,所以才自己处理的。

看 request.py 里的代码,这个 CookieJar 用起来相当不错:

class HTTPCookieProcessor(BaseHandler):
    def __init__(self, cookiejar=None):
        import http.cookiejar
        if cookiejar is None:
            cookiejar = http.cookiejar.CookieJar()
        self.cookiejar = cookiejar

    def http_request(self, request):
        self.cookiejar.add_cookie_header(request)
        return request

    def http_response(self, request, response):
        self.cookiejar.extract_cookies(response, request)
        return response

    https_request = http_request
    https_response = http_response

不过我需要将 cookie 信息保存到文件。从文档上看到有个 FileCookieJar。我尝试了下,出错了,没有 _really_load 方法,我晕。。。之后才注意到其源代码开头有个ASCII图:

						CookieJar____
                        /     \      \
            FileCookieJar      \      \
             /    |   \         \      \
 MozillaCookieJar | LWPCookieJar \      \
                  |               |      \
                  |   ---MSIEBase |       \
                  |  /      |     |        \
                  | /   MSIEDBCookieJar BSDDBCookieJar
                  |/
               MSIECookieJar

原来具体实现还在子类啊。好吧,我就用 MozillaCookieJar 好了。

用法很简单,初始化时把文件名传给它,载入用 load(),保存用 save()。不过要注意的是,文件不存在时不能载入,touch 个空文件出来也不行的。

另外,那个 StackOverflow 的页面还提到了 mechanize 这个模块,有时间去尝试下 :-)

最后,如果我不要它重定向该怎么做呢?难道非要我去用更底层的 http.client?

Category: python | Tags: python 网络

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com