内容


使用 Twisted 框架进行网络编程,第 4 部分

保护客户机与服务器

Comments

在第1、2和3部分中,服务器与客户机具有的共同之处在于它们的操作完全是在一个经过编码的明文会话中进行的。不过在有些时候,您还会希望您的连接能够避开窥视者的眼睛(或者免遭窜改与欺骗)。

用于决定是否允许访问服务器资源的协议是很有意思的,但是在这一部分中,我还是想考虑一下与真正的连接级加密有关的协议。不过在一般的应用背景中,您也许可以研究一下面向Web的机制,例如 RFC-2617 中描述的 Basic Authentication,它在 Apache 和其他的 Web 服务器中都实现了。Twisted 包 twisted.cred 是一种通用且复杂的框架,它在用于一般目的的 Twisted 服务器中提供认证服务器,而并不局限于 Web 服务器。

在 Internet 上进行连接级的加密,有两种广泛应用的 API:SSL 和 SSH。前面一种,SSL(Secure Sockets Layer,安全套接字层),在 Web 浏览器和 Web 服务器中广泛实现;不过从原理上说,SSL并没有理由非得结合到 HTTP 协议上不可。SSL 结合了一种公钥基础设施,连同一个基于 Certificate Authorities 的“可信 Web”(“web-of-trust”)。SSL 会创建一个会话密钥,在某个特定连接的整个生命期中,SSL 都用这个密钥对其进行标准的对称加密。

Twisted 中的确包括了 SSL 框架;不过就和 Twisted 中的大多数东西一样,没有详细的文档来说明 SSL 是如何工作的。我试着下载了两个可能支持的包,尝试让 Twisted v.1.0.6 的脚本 test_ssl.py 运行起来(请看 参考资料),不过在撰写本文的时候还没有成功。

另一个广泛应用于连接级加密的 API 是 SSH(Secure Shell,安全Shell),它因与其同名的那个工具(小写的 ssh )而出名。SSL 与 SSH 共享了很多底层加密算法,不过 SSH 着重于建立加密的 shell 连接(用以取代那些容易受到窥探的程序/协议,比如 telnet 和 rsh)。Twisted 使您能够编写定制的 SSH 客户机和服务器,这可是件非常好的事情。您不仅可以编写一个基本的交互式远程 shell,就好像 ssh 和 sshd 提供的客户机与服务器一样,而且您还可以创建专用性更强的工具,让更高层的应用能够利用这些安全连接。

SSH Weblog 客户机

下面接着讨论本系列文章的例子,我创建了一个工具,用来检查我的 Web 服务器 log 文件中的点击率,不过这次是在一条加密的 SSH 通道上完成的。这个目标实际上也是现实的 -- 也许我不希望监听我的数据包流的人公然看到我的点击率。

Twisted 包自身并没有相应的支持模块,显然也没有确切的文档来说明其原理。在深入工作之前,我需要弄清楚 twisted.conch 包中的 import Crypto 那一行究竟有什么用处。它的名字显然是一种提示,不过我对由 Andrew Kuchling 维护的 Python 加密库也有一定程度的了解(请看 参考资料中的链接)。搜索一下Google,下载,然后安装,Twisted 的 test_conch.py 就很顺利地运行起来了。这样就可以开始创建定制的 SSH 客户机了。

我是基于 Twisted 文件 doc/examples/sshsimpleclient.py 中提供的例子创建客户机的;您也许还想看看那个例子中还有些什么。twisted.conch 像大多数 Twisted 组件一样,包含若干层,其中的每一层都是可以定制的。我猜想“conch”这个名字在安全 Shell 的世界中代替了“shell”的角色。

传输层是一个定制的 SSHClientTransport 。我们可以定义若干个方法,不过至少需要定义 .verifyHostKey().connectionSecure() 。在实现时,我们完全信任所有的主机密钥,只是通过返回一个 defer.succeed 对象,将控制交回给异步反应器(reactor)的核心。当然了,如果您打算根据已知密钥验证某台主机,您可以在 .verifyHostKey() 中实现。

创建通道的过程也就是其他几层加入进来的时候。 SSHUserAuthClient 的子类完成实际的登录认证工作;如果认证成功,它就建立起一条连接(我将连接定义为 SSHConnection 的子类)。这条连接紧接着创建一个通道——即 SSHChannel 的子类。我将这条通道简单命名为 Channel ,实际的定制工作正是通过它来完成的。明确地说就是,这条通道实现了数据和命令的发送与接受。下面让我们来看看我定制的客户机:

清单1. ssh-weblog.py

    #!/usr/bin/env python
"""Monitor a remote weblog over SSH
  USAGE: ssh-weblog.py user@host logfile
"""
from twisted.conch.ssh import transport, userauth, connection, channel
from twisted.conch.ssh.common import NS
from twisted.internet import defer, protocol, reactor
from twisted.python import log
from getpass import getpass
import struct, sys, os
import webloglib as wll
USER,HOST,CMD = None,None,None
class Transport(transport.SSHClientTransport):
    def verifyHostKey(self, hostKey, fingerprint):
        print 'host key fingerprint: %s' % fingerprint
        return defer.succeed(1)
    def connectionSecure(self):
        self.requestService(UserAuth(USER, Connection()))
class UserAuth(userauth.SSHUserAuthClient):
    def getPassword(self):
        return defer.succeed(getpass("password: "))
    def getPublicKey(self):
        return  # Empty implementation: always use password auth
class Connection(connection.SSHConnection):
    def serviceStarted(self):
        self.openChannel(Channel(2**16, 2**15, self))
class Channel(channel.SSHChannel):
    name = 'session'    # must use this exact string
    def openFailed(self, reason):
            print '"%s" failed: %s' % (CMD,reason)
    def channelOpen(self, data):
        self.welcome = data   # Might display/process welcome screen
        d = self.conn.sendRequest(self,'exec',NS(CMD),wantReply=1)
    def dataReceived(self, data):
        recs = data.strip().split('\n')
        for rec in recs:
            hit = [field.strip('"') for field in wll.log_fields(rec)]
            resource = hit[wll.request].split()[1]
            referrer = hit[wll.referrer]
            if resource=='/kill-weblog-monitor':
                print "Bye bye..."
                self.closed()
                return
            elif hit[wll.status]=='200' and hit[wll.referrer]!='-':
                print referrer, ' -->', resource
    def closed(self):
        self.loseConnection()
        reactor.stop()
if __name__=='__main__':
    if len(sys.argv) < 3:
        sys.stderr.write('__doc__')
        sys.exit()
    USER, HOST = sys.argv[1].split('@')
    CMD = 'tail -f -n 1 '+sys.argv[2]
    protocol.ClientCreator(reactor, Transport).connectTCP(HOST, 22)
    reactor.run()

这个客户机的整体结构与我们已经见过的大多数 Twisted 应用程序类似。它首先创建协议,然后在一个异步循环(换句话说就是在 reactor.run() 中)中监视是否有事件发生。

有趣的部分出现在 Channel() 的方法中。通道一旦打开,我们就执行一条定制的命令 -- 本例中是 Weblog 文件中的一条 tail -f 命令,其名称在命令行中指定。这时主机依旧完全是一个一般的 sshd 服务器,而不具有任何 Twisted 特征,它自然而然地会开始发送回一些数据。数据一旦到达, dataReceived() 方法就会对其进行解析(随着 tail 产生更多数据,这个过程也在不断进行)。对于这个特定的客户机而言,我们根据解析出来的 Weblog 的实际内容来决定何时结束 -- 这就相当于一种基于 Web 杀死监视程序的方法。虽然那种特定的配置可能并不常见,但是这个例子还是能够说明如何在某种条件(可以是任何条件)成立的情况下切断连接的基本概念。会话过程如下:

清单2. Weblog 监视器会话实例

$ ./ssh-weblog.py gnosis@gnosis.cx access-log
host key fingerprint: 56:54:76:b6:92:68:85:bb:61:d0:f0:0e:3d:91:ce:34
password:
http://gnosis.cx/publish/  --> /publish/whatsnew.html
http://gnosis.cx/publish/whatsnew.html  --> /home/hugo.gif
Bye bye...

这与本系列文章中创建的其他 Weblog 监视器几乎完全一样。当从另一个窗口中让浏览器转向 <http://gnosis.cx/kill-weblog-monitor> 的时候,上面的会话就结束了(否则,它就会无限期的监视下去)。

修改SSH客户机

如果是出于其他的目的而创建另外的SSH客户机,也是一件很简单的事情。例如,我可以将 ssh-weblog.py 拷贝为 scp.py,并只对代码进行一些修改。 _main_ 主体中解析选项的方式就有些不同,docstring 也进行了调整;此外,我还简单地修改了一下 .dataReceived() 方法,让它来读取数据:

清单3. scp.py (修改过的 Channel 方法)

    def dataReceived(self, data):
    open(DST,'wb').write(data)
    self.closed()

(变量 CMD 的值设置为 "cat "+sys.argv[2] 。)

哈哈!我已经实现了很多 SSH 客户机的 scp 工具。

这些例子都是“运行并收集”类型的工具。也就是说,它们在会话期间并没有交互。不过你可以很简单地创建另一个工具,另外在 Channel 方法中调用 self.conn.sendRequest() 。实际上如果客户机是某种 GUI 客户机,您就可以加入一些数据收集的表单,作为反应器中的回调。换句话说,当某个特定的表单结束之时,也许新的远程命令会发送过来,这时收集结果、然后处理或显示的工作就会再次开始。

SSH Weblog 服务器

SSH 服务器使用的结构与客户机大致相同。与前面一样,我对 doc/examples/sshsimpleserver.py 进行了简化与定制,以适应我的例子。其中一种处理是:服务器最好由用适当的密钥和类配置过的的 SSHFactory 子类创建:

在我们的 SSH Weblog 服务器中,我们为一名经过授权的用户配置了密码和用户名。在本例中,这些配置是硬编码的,但是您显然可以将其存储在其他的地方;比方说可以配置一个授权 Weblog 监视器列表。下面让我们来看看这个例子:

清单4. ssh-weblog-server.py

#!/usr/bin/env python2.3
from twisted.cred import authorizer
from twisted.conch import identity, error
from twisted.conch.ssh import userauth, connection, channel, keys
from twisted.conch.ssh.factory import SSHFactory
from twisted.internet import reactor, protocol, defer
import time
class Identity(identity.ConchIdentity):
    def validatePublicKey(self, data):
        return defer.succeed('')
    def verifyPlainPassword(self, password):
        if password=='password' and self.name == 'user':
            return defer.succeed('')
        return defer.fail(error.ConchError('bad password'))
class Authorizer(authorizer.Authorizer):
    def getIdentityRequest(self, name):
        return defer.succeed(Identity(name, self))
class Connection(connection.SSHConnection):
    def gotGlobalRequest(self, *args):
        return 0
    def getChannel(self, channelType, windowSize, maxPacket, data):
        if channelType == 'session':
            return Channel(remoteWindow=windowSize,
                      remoteMaxPacket=maxPacket, conn=self)
        return 0
class Channel(channel.SSHChannel):
    def channelOpen(self, data):
        weblog = open('../access.log')
        weblog.readlines()
        while 1:
            time.sleep(5)
            for rec in weblog.readlines():
                self.write(rec)
    def request_pty_req(self, data):
        return 1    # ignore, but this gets send for shell requests
    def request_shell(self, data):
        self.client = protocol.Protocol()
        self.client.makeConnection(self)
        self.dataReceived = self.client.dataReceived
        return 1
    def loseConnection(self):
        self.client.connectionLost()
        channel.SSHChannel.loseConnection(self)
class Factory(SSHFactory):
    publicKeys = {'ssh-rsa':keys.getPublicKeyString(
                            data=open('~/.ssh/id_rsa.pub').read())}
    privateKeys ={'ssh-rsa':keys.getPrivateKeyObject(
                            data=open('~/.ssh/id_rsa').read())}
    services = {'ssh-userauth': userauth.SSHUserAuthServer,
                'ssh-connection': Connection}
    authorizer = Authorizer()
reactor.listenTCP(8022, Factory())
reactor.run()

简而言之,我们忽略了解析与格式化 Weblog 记录的操作,不过这种一有新记录就向一个打开的通道写入的思想与客户机的思想方式几乎是相同的。当然了,在这种情况下,任何一般的 SSH 客户机都可以连接到这个专用的服务器。

清单5. Weblog 监视器的示例会话

$ ssh gnosis.python-hosting.com -p 8022 -l user
user@gnosis.python-hosting.com's password:
141.154.146.89 - - [26/Aug/2003:02:47:40 -0500]
"GET /voting-project/August.2003/0010.html HTTP/1.1" 200 8986
"http://gnosis.python-hosting.com/voting-project/August.2003/0009.html"
"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85
(KHTML, like Gecko) Safari/85"
[...]

就像客户机的实现方法一样,您可以在增强版本中加入更多的交互性;还可以定制通道的 .dataReceived() 方法,当数据从(一般的)客户机传来时,就可以作更多有用的工作。

社会问题

很不幸的是,我在推荐 Twisted 框架的时候,最大的保留就是开发团队之间的那种“狂野西部(wild west)”的感觉。这个软件本身是非常强大的。但是即使在大多数开放源代码项目中,不同发布版本之间也缺乏足够的 API 一致性,而且文档都很粗略。最好的方法是从邮件列表中寻求帮助;您或许可以获得有用的答复。

正如写作本文的目的一样,我是在试图填补示例与文档以外的空白,Twisted 确实也在坚持建立能够提供帮助的社区。我真心希望一段时间之后,文档和邮件列表的质量都能够得到改进;特别是那些在犄角旮旯里藏着的 Twisted 框架工具,有些还真是令人印象深刻。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=20734
ArticleTitle=使用 Twisted 框架进行网络编程,第 4 部分
publish-date=12142003