那么有没有一个即方便快捷又安全的登入方式呢?
有,通过 Web Terminal 的方式,通过 Web 的方式即可以避免对客户端的依赖又能够实现用户权限控制。目前,有很多开源的 Web Terminal 的项目,基本上都是通过 SSH 代理的方式调用并返回一个 shell 的虚拟终端(pty)。
实现容器的 Web Terminal
架构图
前端 Web Termianl 页面
Linux 终端返回的内容会带很多特殊的字符,比如我输入一个 ls 指令,终端返回的结果如下:
'l'
's'
'\r\n'
'\x1b[0;0mRUNNING_PID\x1b[0m \x1b[1;34mbin\x1b[0m
\x1b[1;34mconf\x1b[0m \x1b[1;34mlib\x1b[0m\r\nbash-4.3# '
这样我们就需要自己做穷举处理了,这里推荐使用一款的模拟 Terminal 的 JavaScript 库 xterm.js。这个库已经帮我们做了这些复杂操作。
<script>
var term = new Terminal();
term.open(document.getElementById('terminal'));
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
</script>
可以看到它已经将 \x1B[1;3;31mxterm.js\x1B[0m 这些特殊字符变成了红色:
调用 Docker Daemon API 返回 Shell 虚拟终端
在平常的命令行操作下,我们经常会使用 docker exec -i -t /bin/sh 来模拟一个 Shell 的伪终端。在 Web Terminal 实现里,我们需要通过 API 调用的方式来实现同样的操作。当然,我们首先要确保 Docker Daemon 的远程调用是开启的。
先调用 execCreate 来创建一个 Exec。在调用时,需要指定Tty,AttachStdin、AttachStdout和 AttachStderr 参数均为 true,Cmd 参数为 bash,这样才能获得 bash 进程的标准输入输出和错误;
request:
POST /v1.24/containers/e90e34656806/exec HTTP/1.1
Content-Type: application/json
{
"AttachStdin": true,
"AttachStdout": true,
"AttachStderr": true,
"Cmd": ["sh"],
"DetachKeys": "ctrl-p,ctrl-q",
"Tty": true,
...
}
如果调用 execCreate 成功,调用请求会返回该 Exec 的 ID,根据这个 ID 继续调用execStart 接口。在调用时,需要指定 Detach 为 False,Tty 为 True,这样才会返回一个 HTTP 的 stream:
request:
POST /v1.24/exec/e90e34656806/start HTTP/1.1
Content-Type: application/json
{
"Detach": false,
"Tty": true
} }}}
response:
{{{HTTP/1.1 200 OK
Content-Type: application/vnd.docker.raw-stream
{{ STREAM }}
d-terminal
d-terminal 是这个系统的核心,它分成两个部分:
一部分用于处理用户端的输入和输出,以及存储和展示后端 Docker Dameon 主机的 IP 和 container_id。因为像 top 这样的监控命令需要服务端定时推送数据给客户端,所以使用了 WebSocket 协议以支持服务端推送。
另一部分用于调用 Docker Daemon 返回虚拟终端。对于终端来说,通常是你输入一个字符就会立马返回,直到你输入一个 "归位键" 终端才会把你输入的字符拼接成一个字符串并发送给 shell 解释器,并将 shell 解释器的结果返回。为了提升使用流畅性,新启用了一个线程去调用 Docker Daemon API,当然也可以使用像 epoll 这样的多路复用技术来实现。
d-terminal 是使用 Python 实现的 Web 应用,核心代码如下:
@sockets.route('/echo')
def echo_socket(ws):
...
# 调用 Docker API 返回一个虚拟终端
docker_daemon_sock = get_tty()._sock
# 启动一个与 Docker Daemon 通讯的子线
docker_daemon_sock_thd = DockerDaemonSock(ws, docker_daemon_sock)
docker_daemon_sock_thd.start()
while not ws.closed:
message = ws.receive() # 接收 terminal 的输入
# 将用户的输入发送那个 docker daemon
docker_daemon_sock.send(bytes(message, encoding='utf-8'))
# 子线程 DockerDaemonSock 类
class DockerDaemonSock(threading.Thread):
def __init__(self, ws, docker_daemon_sock):
super(DockerDaemonSock, self).__init__()
self.ws = ws
self.docker_daemon_sock = docker_daemon_sock
def run(self):
while not self.ws.closed:
try:
# 接收 docker daemon 的返回
resp = self.docker_daemon_sock.recv(2048)
if resp:
# 将 docker daemon 的返回发送给前端 terminal
self.ws.send(str(resp, encoding='utf-8'))
else:
print("docker socket closed.")
self.ws.close()
except Exception as e:
print("docker termial socket error: {}".format(e))
self.ws.close()
总结
最后,上述的 demo 可去 GitHub 具体查看。效果如下:
参考:《 一种新的进入容器的方式: WebSocket + Docker Remote API 》
本文转载自公众号:杏仁技术站, 点击查看原文。