由浅入深写代理(6)-http-代理.md

本文讲 http 代理,顾名思义,http 代理代理的是 http 请求,其实这里面分两类

* 普通代理 这种代理扮演的是「中间人」角色,对于连接到它的客户端来说,它是服务端;对于要连接的服务端来说,它是客户端。它就负责在两端之间来回传送 HTTP 报文。
* 隧道代理。它通过 HTTP 协议正文部分(Body)完成通讯,以 HTTP 的方式实现任意基于 TCP 的应用层协议代理。这种代理使用 HTTP 的 CONNECT 方法建立连接。

通俗一点讲,普通代理解析 http 包,然后将请求转发到目标地址,但是没法解析 https 的包,所以也就没法代理 https 的请求,但是隧道代理可以代理 https 的请求或者其他的一些协议请求。

0x01 普通代理

代码很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import socket
from urllib.parse import urlparse
from http.server import BaseHTTPRequestHandler, HTTPServer
class ProxyHandler(BaseHTTPRequestHandler):
def _recv_data_from_remote(self, sock):
data = b''
while True:
recv_data = sock.recv(4096)
if not recv_data:
break
data += recv_data
sock.close()
return data
def do_GET(self):
# 解析 GET 请求信息
uri = urlparse(self.path)
scheme, host, path = uri.scheme, uri.hostname, uri.path
host_ip = socket.gethostbyname(host)
port = 443 if scheme == "https" else 80
# 为了简单起见,Connection 都为 close, 也就不需要 Proxy-Connection 判断了
del self.headers['Proxy-Connection']
self.headers['Connection'] = 'close'
# 构造新的 http 请求
send_data = "GET {path} {protocol_version}\r\n".format(path=path, protocol_version=self.protocol_version)
headers = ''
for key, value in self.headers.items():
headers += "{key}: {value}\r\n".format(key=key, value=value)
headers += '\r\n'
send_data += headers
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host_ip, port))
# 发送请求到目标地址
sock.sendall(send_data.encode())
data = self._recv_data_from_remote(sock)
self.wfile.write(data)
def main():
try:
server = HTTPServer(('', 8888), ProxyHandler)
server.serve_forever()
except KeyboardInterrupt:
server.socket.close()
if __name__ == '__main__':
main()

这里面就实现了 get 请求的转发,只用单线程的方式来处理,其他的有兴趣的同学可以自己扩展下。
看下效果

1
python3 http_server.py

配置完代理后可以发现 http 的请求都能正常转发,但是 https 的都没法识别。

下篇教程看如何通过隧道代理解决这个问题。

参考资料

* https://imququ.com/post/web-proxy.html
* http://www.lyyyuna.com/2016/01/16/http-proxy-get1/

由浅入深写代理(5)-socks5-代理.md

既然 socks5 是协议,所以客户端也必须有 socks5 的实现才能和服务端连接。这里面我就用现成的开源库 PySocks 来演示下。

pySocks 有两种方式来实现,一种是直接的 s = socks.socksocket() 创建一个新的 socket 对象,还有一种是 Monkeypatching,算是 python 的黑魔法。

我们用第一种方法试下
首先把代理服务器开起来

1
python3 socks5_server.py

然后运行如下代码

1
2
3
4
5
6
7
import socks
s = socks.socksocket() # Same API as socket.socket in the standard lib
s.set_proxy(socks.SOCKS5, "localhost", 8888)
s.connect(("www.weibo.com", 80))
s.sendall("GET / HTTP/1.1 /r/n/r/n")
print s.recv(4096)

返回值如下

由于没有设置 http 正确的 header,所以 weibo 服务器返回了 400

当然你可以直接用 chrome 的 代理插件设置如下

可以看到所有的请求都经过 socks5 服务器了

由浅入深写代理(4)-socks5-代理.md

接下来我们用 python 写下 socks5 服务端的实现

0x03 socks5 实现

先看下整体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import logging
import socket
import struct
import select
import threading
def send_data(sock, data):
print(data)
bytes_sent = 0
while True:
r = sock.send(data[bytes_sent:])
if r < 0:
return r
bytes_sent += r
if bytes_sent == len(data):
return bytes_sent
def handle_tcp(sock, remote):
# 处理 client socket 和 remote socket 的数据流
try:
fdset = [sock, remote]
while True:
# 用 IO 多路复用 select 监听套接字是否有数据流
r, w, e = select.select(fdset, [], [])
if sock in r:
data = sock.recv(4096)
if len(data) <= 0:
break
result = send_data(remote, data)
if result < len(data):
raise Exception('failed to send all data')
if remote in r:
data = remote.recv(4096)
if len(data) <= 0:
break
result = send_data(sock, data)
if result < len(data):
raise Exception('failed to send all data')
except Exception as e:
raise(e)
finally:
sock.close()
remote.close()
def handle_con(sock, addr):
# 接受客户端来的请求,socks5 的 认证和连接过程
sock.recv(256)
# 无需进一步认证信息
sock.send(b"\x05\x00")
data = sock.recv(4) or '\x00' * 4
# CMD 为 0x01 也就是 CONNECT 继续
mode = data[1]
if mode != 1:
return
# DST.ADDR 有三种形式,分别做判断
addr_type = data[3]
if addr_type == 1:
addr_ip = sock.recv(4)
remote_addr = socket.inet_ntoa(addr_ip)
elif addr_type == 3:
addr_len = int.from_bytes(sock.recv(1), byteorder='big')
remote_addr = sock.recv(addr_len)
elif addr_type == 4:
addr_ip = sock.recv(16)
remote_addr = socket.inet_ntop(socket.AF_INET6, addr_ip)
else:
return
# DST.PORT
remote_addr_port = struct.unpack('>H', sock.recv(2))
# 返回给客户端 success
reply = b"\x05\x00\x00\x01"
reply += socket.inet_aton('0.0.0.0') + struct.pack(">H", 8888)
sock.send(reply)
# 拿到 remote address 的信息后,建立连接
try:
remote = socket.create_connection((remote_addr, remote_addr_port[0]))
logging.info('connecting %s:%d' % (remote_addr, remote_addr_port[0]))
except socket.error as e:
logging.error(e)
return
handle_tcp(sock, remote)
def main():
socketServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socketServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
socketServer.bind(('', 8888))
socketServer.listen(5)
try:
while True:
sock, addr = socketServer.accept()
t = threading.Thread(target=handle_con, args=(sock, addr))
t.start()
except socket.error as e:
logging.error(e)
except KeyboardInterrupt:
socketServer.close()
if __name__ == '__main__':
main()

首先,上面只是个 socks5 服务器的简单实现,处理 TCP 的转发,无需密码认证。

接下来我们分开来看,主要是三个部分 main(), handle_con(), handle_tcp()

  • main() 函数大家其实挺熟悉的,就是前面教程介绍的 socket 编程的线程方法,绑定套接字监听,然后有客户端请求后,调用 handle_con()
  • handle_con 实现了 socks5 的 认证和连接过程,大家对照下注释和前面的 socks5 协议,应该挺容易看懂。
  • 认证完成后,就开始转发请求了, handle_tcp() 实现了这个功能,这里面用到了 select ,IO 多路复用模式。其实一开始怕大家不理解,也想用线程来实现,不过发现 IO 多路复用在一个线程中就能监听多个套接字,代码比多线程方式更加简洁,索性就这么用了。这里面没有根据平台去判断用 select, poll 还是 epoll,感兴趣的话可以直接看 shadowsocks 的实现 eventloop

下篇我们看看 socks5 服务器如何和 client 端交互。

参考链接:

* https://github.com/felix021/ssocks5/blob/master/ssocks5.py
* https://github.com/RicterZ/reprocks/blob/master/server/reprocks_server.py

由浅入深写代理(3) -socks5 代理

本文讲的是如何写一个 socks5 代理,其实 shadowsocks 的代理也是 socks5 协议的,所以 socks5 代理也是本系列教程的一个重点。

首先放出 socks5 协议的 rfc,socks5 协议很简单,SOCKS5 协议并不负责代理服务器的数据传输环节,此协议只是在 C/S 两端真实交互之间,建立起一条从客户端到代理服务器的授信连接。

sock5 代理结构图

0x02 socks5 协议分析

认证阶段

首先客户端需要和服务端有个握手认证的过程,可以采用 用户名/密码 认证或者无需认证方式。

格式如下 (数字表示位数

+----+----------+----------+
|VER | NMETHODS | METHODS  |
+----+----------+----------+
| 1  |    1     |  1~255   |
+----+----------+----------+
  • VER 字段是当前协议的版本号,也就是 5;
  • NMETHODS 字段是 METHODS 字段占用的字节数;
  • METHODS 字段的每一个字节表示一种认证方式,表示客户端支持的全部认证方式。

    0x00: NO AUTHENTICATION REQUIRED
    0x01: GSSAPI
    0x02: USERNAME/PASSWORD
    0x03: to X’7F’ IANA ASSIGNED
    0x80: to X’FE’ RESERVED FOR PRIVATE METHODS
    0xFF: NO ACCEPTABLE METHODS
    

服务端返回格式

+----+--------+
|VER | METHOD |
+----+--------+
| 1  |   1    |
+----+--------+

一般情况下服务端返回两种情况

0x05 0x00:告诉客户端采用无认证的方式建立连接;
0x05 0xff:客户端的任意一种认证方式服务器都不支持。

举个例子, 服务器无需认证的情况如下

client -> server: 0x05 0x01 0x00
server -> client: 0x05 0x00
连接阶段

认证完成,客户端向服务端发送请求:

+----+-----+-------+------+----------+----------+
|VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  |   1   |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+
  • CMD 字段 command 的缩写:
    • 0x01:CONNECT 建立 TCP 连接
    • 0x02: BIND 上报反向连接地址
    • 0x03:关联 UDP 请求
  • RSV 字段:保留字段,值为 0x00
  • ATYP 字段:address type 的缩写,取值为:
    • 0x01:IPv4
    • 0x03:域名
    • 0x04:IPv6
  • DST.ADDR 字段:destination address 的缩写,取值随 ATYP 变化:
    • ATYP == 0x01:4 个字节的 IPv4 地址
    • ATYP == 0x03:1 个字节表示域名长度,紧随其后的是对应的域名
    • ATYP == 0x04:16 个字节的 IPv6 地址
    • DST.PORT 字段:目的服务器的端口

服务端返回格式

+----+-----+-------+------+----------+----------+
|VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  |   1   |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+
  • REP 字段
    • X’00’ succeeded
    • X’01’ general SOCKS server failure
    • X’02’ connection not allowed by ruleset
    • X’03’ Network unreachable
    • X’04’ Host unreachable
    • X’05’ Connection refused
    • X’06’ TTL expired
    • X’07’ Command not supported
    • X’08’ Address type not supported
    • X’09’ to X’FF’ unassigned

举个例子,客户端通过 127.0.0.1:8000 的代理发送请求

# request:        VER  CMD  RSV  ATYP DST.ADDR            DST.PORT
client -> server: 0x05 0x01 0x00 0x01 0x7f 0x00 0x00 0x01 0x1f 0x40
# response:       VER  REP  RSV  ATYP BND.ADDR            BND.PORT
server -> client: 0x05 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x10 0x10

传输阶段

接下来就开始传输数据,socks5 服务器只做单纯的转发功能

整个过程如下

# 认证阶段
client -> server: 0x05 0x01 0x00
server -> client: 0x05 0x00
# 连接阶段
client -> server: 0x05 0x01 0x00 0x03 0x0a b'google.com'  0x00 0x50
server -> client: 0x05 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x10 0x10
# 传输阶段
client -> server -> remote
remote -> server -> client
...    

下篇教程用代码实现下 socks5 代理

参考链接:

* https://loggerhead.me/posts/shadowsocks-yuan-ma-fen-xi-xie-yi-yu-jie-gou.html#fn:bnd.addr
* https://www.ietf.org/rfc/rfc1928.txt
* http://www.moye.me/2017/08/03/analyze-socks5-protocol/

由浅入深写代理(2)- socket 编程

说到代理,那肯定会跟网络协议有关,包括(tcp, ip, http),网络中的进程需要通过 socket 来通信,socket 可以认为是操作系统抽象出来的一类接口,供使用者能够更加方便的与底层的网络协议打交道。

0x01
我们先来看看 tcp 的 socket 编程。

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import socket
import threading
# AF_INET: 基于 IPV4 的网络通信 SOCK_STREAM: 基于 TCP 的流式 socket 通信
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 将套接字绑定到地址
s.bind(('127.0.0.1', 8888))
# 监听TCP传入连接
s.listen(5)
def handle_tcp(sock, addr):
print("new connection from %s:%s" % addr)
sock.send(b'Welcome!')
while True:
data = sock.recv(1024)
if not data:
break
sock.send(b'Hello, %s!' % data)
sock.close()
while True:
sock, addr = s.accept()
t = threading.Thread(target=handle_tcp, args=(sock, addr))
t.start()

客户端

1
2
3
4
5
6
7
8
9
10
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 8888))
print(s.recv(1024))
for data in [b'dog']:
s.send(data)
print(s.recv(1024))
s.close()

上面是个很简单的客户端和服务端的例子,服务器端用了线程,主要是为了能够同时处理多个请求。不然每次处理请求的时候整个程序就会处于阻塞状态。

通过 wireshark 捕获请求,可以看到客户端经过三次握手和服务端连接成功,接下来双方开始发送数据,发送完成后,四次挥手断开连接。

具体说下过程吧

  1. 首先服务器端初始化一个 socket 对象,将 socket 绑定到 (127.0.0.1, 8888) 这个地址上(bind),然后开始监听(listen),并阻塞在 accept 函数上,直到有连接过来。
  2. 客户端也初始化一个 socket 对象,调用 connect 和服务端建立连接。
  3. 服务端 accept 函数返回了一个新的 sock 套接字对象,传入到新线程中和客户端交互数据。
  4. 接下来就是 socket 的 recv 和 send 函数进行数据的交互。
  5. 最后 socket close 关闭套接字。

由于 tcp 传递的数据属于 stream, 也就是调用 recv 和 send 的次数都没有限制,对数据的发送和边界也没有限制。这个和下文的 udp 编程有区别,发送端每执行一次写操作,udp 模块就会将它封装成一个 udp 包发送,接收端也对每个 udp 包执行一次读操作,每次都得完整取出来,如果没有足够的应用缓冲区来读取 udp 数据包,则会被截断。

0x02

再简单看下 udp 的 socket 编程

服务端

1
2
3
4
5
6
7
8
9
10
11
import socket
# AF_INET: 基于 IPV4 的网络通信 SOCK_DGRAM: 基于 udp 的流式 socket 通信
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 将套接字绑定到地址
s.bind(('127.0.0.1', 8888))
while True:
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
s.sendto(b'Hello, %s!' % data, addr)

客户端

1
2
3
4
5
6
7
8
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [b'dog']:
s.sendto(data, ('127.0.0.1', 8888))
print(s.recv(1024))
s.close()

由于 udp 不需要建立连接,只需要知道对方的 IP 地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。所以客户端直接通过 sendto() 给服务器发数据,服务端调用 recvfrom() 就能拿到数据。