为了说明需要做什么,请使用以下 Python 程序。我使用 Python 只是因为它易于阅读,但在 C 中也可以做到这一点,只是代码行更多且更难阅读。
让我们首先进行一些初始化,即创建一些套接字服务器、SSL 上下文、接受新客户端、将客户端 fd 包装到 SSL 套接字中并在客户端和套接字服务器之间进行一些初始通信。根据您之前的问题,您可能已经知道如何在 C 中执行此操作,并且 Python 代码与您在 C 中所做的相距不远:
import socket
import ssl
import select
import os
local_addr = ('',8888) # where we listen
cmd = ['./cmd.pl'] # do some command reading stdin, writing stdout
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
ctx.load_cert_chain('server_cert_and_key.pem')
srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(local_addr)
srv.listen(10)
try:
cl,addr = srv.accept()
except:
pass
cl = ctx.wrap_socket(cl,server_side=True)
print("new connection from {}".format(addr))
buf = cl.read(1024)
print("received '{}'".format(buf))
cl.write("hi!\n")
完成此设置并且我们与客户端建立 SSL 连接后,我们将分叉程序。该程序将读取标准输入并写入标准输出,我们希望将来自 SSL 客户端的解密输入作为程序的输入转发,并将加密的程序输出转发给 SSL 客户端。为此,我们创建了一个套接字对,用于在父进程和派生程序之间交换数据,并将套接字对的一侧重新映射到 execv 程序之前派生子进程中的 stdin/stdout。这也与 C 非常相似:
print("forking subprocess")
tokid, toparent = socket.socketpair()
pid = os.fork()
if pid == 0:
tokid.close()
# child: remap stdin/stdout and exec cmd
os.dup2(toparent.fileno(),0)
os.dup2(toparent.fileno(),1)
toparent.close()
os.execv(cmd[0],cmd)
# will not return
# parent process
toparent.close()
现在我们需要从 SSL 客户端和分叉命令读取数据并将其转发到另一端。虽然有人可能会通过在线程内阻塞读取来做到这一点,但我更喜欢使用 select 基于事件。 Python 中的 select 语法与 C 中的语法有点不同(即更简单),但想法完全相同:我们调用它的方式将在我们从客户端或分叉命令获得数据时返回,或者如果一秒钟内没有数据已过:
# parent: wait for data from client and subprocess and forward these to
# subprocess and client
done = False
while not done:
readable,_,_ = select.select([ cl,tokid ], [], [], 1)
if not readable:
print("no data for one second")
continue
由于readable 不为空,我们有新数据等待我们读取。在 Python 中,我们在文件句柄上使用 recv,在 C 中,我们需要在 SSL 套接字上使用 SSL_read,在普通套接字(来自套接字对)上使用 recv 或 read。读取数据后,我们将其写入另一端。在 Python 中,我们可以使用 sendall,在 C 中,我们需要在 SSL 套接字上使用 SSL_write,在普通套接字上使用 send 或 write - 我们还需要确保所有数据都已写入,即也许尝试多次。
将 select 与 SSL 套接字结合使用时有一件值得注意的事情。如果您SSL_read 小于最大 SSL 帧大小,则可能是 SSL 帧的有效负载大于SSL_read 中请求的负载。在这种情况下,OpenSSL 将在内部缓冲剩余的数据,并且即使有已经缓冲的数据,下一次调用 select 也可能不会显示更多可用数据。要解决这个问题,要么需要检查 SSL_pending 的缓冲数据,要么只使用 SSL_read 始终使用最大 SSL 帧大小:
for fd in readable:
# Always try to read 16k since this is the maximum size for an
# SSL frame. With lower read sizes we would need to explicitly
# deal with pending data from SSL (man SSL_pending)
buf = fd.recv(16384)
print("got {} bytes from {}".format(len(buf),"client" if fd == cl else "subprocess"))
writeto = tokid if fd == cl else cl
if buf == '':
# eof
writeto.close()
done = True
break # return from program
else:
writeto.sendall(buf)
print("connection done")
就是这样。完整程序也可以在here 获得,我用来测试的小程序也可以在here 获得。