警告

本节包含从C++自动翻译成Python的代码片段,可能存在错误。

DTLS服务器#

本示例演示了如何实现一个简单的DTLS服务器。

../_images/secureudpserver-example.png

注意

DTLS服务器示例旨在与DTLS客户端示例同时运行。

服务器由DtlsServer类实现。它使用QUdpSocketQDtlsClientVerifierQDtls来测试每个客户端的可达性,完成握手,并读取和写入加密的消息。

class DtlsServer(QObject):

    Q_OBJECT
# public
    DtlsServer()
    ~DtlsServer()
    listen = bool(QHostAddress address, quint16 port)
    isListening = bool()
    def close():
# signals
    def errorMessage(message):
    def warningMessage(message):
    def infoMessage(message):
    def datagramReceived(peerInfo, cipherText,):
                          plainText) = QByteArray()
# private slots
    def readyRead():
    def pskRequired(auth):
# private
    def handleNewConnection(peerAddress, peerPort,):
                             clientHello) = QByteArray()
    def doHandshake(newConnection, clientHello):
    def decryptDatagram(connection, clientMessage):
    def shutdown():
    listening = False
    serverSocket = QUdpSocket()
    serverConfiguration = QSslConfiguration()
    cookieSender = QDtlsClientVerifier()
    std.vector<std.unique_ptr<QDtls>> knownClients
    Q_DISABLE_COPY(DtlsServer)

构造函数连接了QUdpSocket::readyRead()信号到其readyRead()槽,并设置了所需的TLS配置。

def __init__(self):

    serverSocket.readyRead.connect(self.readyRead)
    serverConfiguration = QSslConfiguration.defaultDtlsConfiguration()
    serverConfiguration.setPreSharedKeyIdentityHint("Qt DTLS example server")
    serverConfiguration.setPeerVerifyMode(QSslSocket.VerifyNone)

注意

服务器没有使用证书,依赖于预共享密钥(PSK)握手。

listen()绑定QUdpSocket

def listen(self, QHostAddress address, quint16 port):

    if address != serverSocket.localAddress() or port != serverSocket.localPort():
        shutdown()
        listening = serverSocket.bind(address, port)
        if not listening:
            errorMessage.emit(serverSocket.errorString())
    else:
        listening = True

    return listening

readyRead()槽处理传入的数据报

    ...

bytesToRead = serverSocket.pendingDatagramSize()
if bytesToRead <= 0:
    warningMessage.emit(tr("A spurious read notification"))
    return

dgram = QByteArray(bytesToRead, Qt.Uninitialized)
peerAddress = QHostAddress()
peerPort = 0
bytesRead = serverSocket.readDatagram(dgram.data(), dgram.size(),
                                                   peerAddress, peerPort)
if bytesRead <= 0:
    warningMessage.emit(tr("Failed to read a datagram: ") + serverSocket.errorString())
    return

dgram.resize(bytesRead)            ...

在提取地址和端口号后,服务器首先检查该数据报是否来自已知的对等端

    ...

if peerAddress.isNull() or not peerPort:
    warningMessage.emit(tr("Failed to extract peer info (address, port)"))
    return

client = std::find_if(knownClients.begin(), knownClients.end(),
                                 [](std.unique_ptr<QDtls> connection){
    return connection.peerAddress() == peerAddress
           and connection.peerPort() == peerPort
})            ...

如果是一个新的、未知的地址和端口号,则将该数据报处理为可能来自DTLS客户端的ClientHello消息

    ...

if client == knownClients.end():
    return handleNewConnection(peerAddress, peerPort, dgram)            ...

如果是一个已知的DTLS客户端,服务器要么解密数据报

    ...

if (client).isConnectionEncrypted():
    decryptDatagram(client.get(), dgram)
    if (client).dtlsError() == QDtlsError.RemoteClosedConnectionError:
        knownClients.erase(client)
    return            ...

或者与该对等端继续握手

    ...

doHandshake(client.get(), dgram)            ...

handleNewConnection()验证它是一个可达的DTLS客户端,或者发送HelloVerifyRequest

def handleNewConnection(self, peerAddress,):
                                     quint16 peerPort, QByteArray clientHello)

    if not listening:
        return
    peerInfo = peer_info(peerAddress, peerPort)
    if cookieSender.verifyClient(serverSocket, clientHello, peerAddress, peerPort):
        infoMessage.emit(peerInfo + tr(": verified, starting a handshake"))            ...

如果验证新客户端为可达的DTLS客户端,服务器创建并配置一个新的 QDtls对象,并开始服务器端的握手

    ...

std.unique_ptr<QDtls> newConnection{QDtls(){QSslSocket.SslServerMode}}
newConnection.setDtlsConfiguration(serverConfiguration)
newConnection.setPeer(peerAddress, peerPort)
newConnection.connect(newConnection.get(), QDtls.pskRequired,
                       self.pskRequired)
knownClients.push_back(std.move(newConnection))
doHandshake(knownClients.back().get(), clientHello)            ...

doHandshake()进度通过握手阶段

def doHandshake(self, newConnection, clientHello):

    result = newConnection.doHandshake(serverSocket, clientHello)
    if not result:
        errorMessage.emit(newConnection.dtlsErrorString())
        return

    peerInfo = peer_info(newConnection.peerAddress(),
                                       newConnection.peerPort())
    switch (newConnection.handshakeState()) {
    elif graphicsApi == QDtls.HandshakeInProgress:
        infoMessage.emit(peerInfo + tr(": handshake is in progress ..."))
        break
    elif graphicsApi == QDtls.HandshakeComplete:
        infoMessage.emit(tr("Connection with %1 encrypted. %2")
                         .arg(peerInfo, connection_info(newConnection)))
        break
    else:
        Q_UNREACHABLE()

在握手阶段,会发出pskRequired()信号,而pskRequired()槽提供了预共享密钥

def pskRequired(self, auth):

    Q_ASSERT(auth)
    infoMessage.emit(tr("PSK callback, received a client's identity: '%1'")
                     .arg(QString.fromLatin1(auth.identity())))
    auth.setPreSharedKey(QByteArrayLiteral("\x1a\x2b\x3c\x4d\x5e\x6f"))

注意

为了简明扼要,pskRequired()的定义过于简化。有关QSslPreSharedKeyAuthenticator类的详细说明,请参考文档。

当网络对等端的握手完成后,一个加密的DTLS连接被认为是已建立的,服务器通过调用decryptDatagram()解密由对等端发送的后续数据报。服务器也会向对等端发送加密响应。

def decryptDatagram(self, connection, clientMessage):

    Q_ASSERT(connection.isConnectionEncrypted())
    peerInfo = peer_info(connection.peerAddress(), connection.peerPort())
    dgram = connection.decryptDatagram(serverSocket, clientMessage)
    if dgram.size():
        datagramReceived.emit(peerInfo, clientMessage, dgram)
        connection.writeDatagramEncrypted(serverSocket, tr("to %1: ACK").arg(peerInfo).toLatin1())
     elif connection.dtlsError() == QDtlsError.NoError:
        warningMessage.emit(peerInfo + ": " + tr("0 byte dgram, could be a re-connect attempt?"))
    else:
        errorMessage.emit(peerInfo + ": " + connection.dtlsErrorString())

服务器通过调用 shutdown() 来关闭其DTLS连接

def shutdown(self):

    for connection in std::exchange(knownClients, {}):
        connection.shutdown(serverSocket)
    serverSocket.close()

在其运行过程中,服务器通过发射errorMessage()、warningMessage()、infoMessage()和datagramReceived()信号来报告错误、信息消息和解密的数据报。这些消息由服务器界面进行记录

colorizer = QString(QStringLiteral("<font color=\"%1\">%2"))
def addErrorMessage(self, message):

    ui.serverInfo.insertHtml(colorizer.arg("Crimson", message))

def addWarningMessage(self, message):

    ui.serverInfo.insertHtml(colorizer.arg("DarkOrange", message))

def addInfoMessage(self, message):

    ui.serverInfo.insertHtml(colorizer.arg("DarkBlue", message))

def addClientMessage(self, peerInfo, datagram,):
                                  QByteArray plainText)

    messageColor = "DarkMagenta"
    formatter = QStringLiteral("<br>---------------"()
                                                    "<br>A message from %1"
                                                    "<br>DTLS datagram:<br> %2"
                                                    "<br>As plain text:<br> %3")
    html = formatter.arg(peerInfo, QString.fromUtf8(datagram.toHex(' ')),
                                       QString.fromUtf8(plainText))
    ui.messages.insertHtml(colorizer.arg(messageColor, html))

示例项目 @ code.qt.io