asyncio之使用SSL

使用SSL

asyncio内置支持在socket上启用SSL通信。将SSLContext实例传递给创建服务器或客户端连接的协程将启用该支持,并在socket准备好供应用程序使用之前,确保SSL协议设置得当。
来自上一节的基于协程的echo服务器和客户端将在这里进行一些小修改。第一步是创建证书和密钥文件。自签名证书可以使用如下命令创建:

1
$ openssl req -newkey rsa:2048 -nodes -keyout pymotw.key -x509 -days 365 -out pymotw.crt

openssl命令将提示用于生成证书的多个值,然后生成请求的输出文件。
在以前的服务器示例中,不安全的socket设置使用start_server()来创建监听socket。

1
2
factory = asyncio.start_server(echo, *SERVER_ADDRESS)
server = event_loop.run_until_complete(factory)

要添加加密,请使用刚刚生成的证书和密钥创建一个SSLContext,然后将该上下文传递给start_server()。

1
2
3
4
5
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.check_hostname = False
ssl_context.load_cert_chain('pymotw.crt', 'pymotw.key')
factory = asyncio.start_server(echo, *SERVER_ADDRESS, ssl=ssl_context)

客户端需要进行类似的更改。旧版本使用open_connection()来创建连接到服务器的socket。

1
reader, writer = await asyncio.open_connection(*address)

SSLContext需要再次保护socket的客户端。客户端身份没有被强制执行,所以只需要加载证书。

1
2
3
4
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,)
ssl_context.check_hostname = False
ssl_context.load_verify_locations('pymotw.crt')
reader, writer = await asyncio.open_connection(*address, ssl=ssl_context)

另外一个需要在客户端做一些修改的是,由于SSL连接不支持发送文件结尾(EOF),客户端将使用NULL字节作为消息终止符。
旧版本的客户端使用write_eof()发送循环。

1
2
3
4
5
6
for msg in messages:
writer.write(msg)
log.debug('sending {!r}'.format(msg))
if writer.can_write_eof():
writer.write_eof()
await writer.drain()

新版本发送0字节(b’\x00’)。

1
2
3
4
5
for msg in messages:
writer.write(msg)
log.debug('sending {!r}'.format(msg))
writer.write(b'\x00')
await writer.drain()

服务器中的echo()协程必须查找NULL字节并在收到时关闭客户端连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async def echo(reader, writer):
address = writer.get_extra_info('peername')
log = logging.getLogger('echo_{}_{}'.format(*address))
log.debug('connection accepted')
while True:
data = await reader.read(128)
terminate = data.endswith(b'\x00')
data = data.rstrip(b'\x00')
if data:
log.debug('received {!r}'.format(data))
writer.write(data)
await writer.drain()
log.debug('sent {!r}'.format(data))
if not data or terminate:
log.debug('message terminated, closing connection')
writer.close()
return

在一个窗口中运行服务器,在另一个窗口中运行客户端,生成此输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ python3 asyncio_echo_server_ssl.py
asyncio: Using selector: KqueueSelector
main: starting up on localhost port 10000
echo_::1_53957: connection accepted
echo_::1_53957: received b'This is the message. '
echo_::1_53957: sent b'This is the message. '
echo_::1_53957: received b'It will be sent in parts.'
echo_::1_53957: sent b'It will be sent in parts.'
echo_::1_53957: message terminated, closing connection
$ python3 asyncio_echo_client_ssl.py
asyncio: Using selector: KqueueSelector
echo_client: connecting to localhost port 10000
echo_client: sending b'This is the message. '
echo_client: sending b'It will be sent '
echo_client: sending b'in parts.'
echo_client: waiting for response
echo_client: received b'This is the message. '
echo_client: received b'It will be sent in parts.'
echo_client: closing
main: closing event loop

附上完整源码

TCP Echo Server:

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
#coding=UTF-8
import asyncio
import logging
import sys
import ssl
logging.basicConfig(
level=logging.DEBUG,
format='%(name)s: %(message)s',
stream=sys.stderr,
)
log = logging.getLogger('main')
async def echo(reader, writer):
address = writer.get_extra_info('peername')
log = logging.getLogger('echo_{}_{}'.format(*address))
log.debug('connection accepted')
while True:
data = await reader.read(128)
terminate = data.endswith(b'\x00')
data = data.rstrip(b'\x00')
if data:
log.debug('received {!r}'.format(data))
writer.write(data)
await writer.drain()
log.debug('sent {!r}'.format(data))
if not data or terminate:
log.debug('message terminated, closing connection')
writer.close()
return
SERVER_ADDRESS = ('localhost', 10000)
event_loop = asyncio.get_event_loop()
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.check_hostname = False
ssl_context.load_cert_chain('pymotw.crt', 'pymotw.key')
factory = asyncio.start_server(echo, *SERVER_ADDRESS, ssl=ssl_context)
server = event_loop.run_until_complete(factory)
log.debug('starting up on {} port {}'.format(*SERVER_ADDRESS))
try:
event_loop.run_forever()
except KeyboardInterrupt:
pass
finally:
log.debug('closing server')
server.close()
event_loop.run_until_complete(server.wait_closed())
log.debug('closing event loop')
event_loop.close()

TCP Echo Client:

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
#coding=UTF-8
import asyncio
import logging
import sys
import ssl
MESSAGES = [
b'This is the message. ',
b'It will be sent ',
b'in parts.',
]
logging.basicConfig(
level=logging.DEBUG,
format='%(name)s: %(message)s',
stream=sys.stderr,
)
log = logging.getLogger('main')
async def echo_client(address, messages):
log = logging.getLogger('echo_client')
log.debug('connecting to {} port {}'.format(*address))
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,)
ssl_context.check_hostname = False
ssl_context.load_verify_locations('pymotw.crt')
reader, writer = await asyncio.open_connection(*address, ssl=ssl_context)
for msg in messages:
writer.write(msg)
log.debug('sending {!r}'.format(msg))
writer.write(b'\x00')
await writer.drain()
log.debug('waiting for response')
while True:
data = await reader.read(128)
if data:
log.debug('received {!r}'.format(data))
else:
log.debug('closing')
writer.close()
return
SERVER_ADDRESS = ('localhost', 10000)
event_loop = asyncio.get_event_loop()
try:
event_loop.run_until_complete(
echo_client(SERVER_ADDRESS, MESSAGES)
)
finally:
log.debug('closing event loop')
event_loop.close()

本文翻译自《The Python3 Standard Library By Example》asyncio相关章节

文章目录
  1. 1. 附上完整源码
|