1 /*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2000 Alex Zepeda <zipzippy@sonic.net>
4 SPDX-FileCopyrightText: 2001-2003 George Staikos <staikos@kde.org>
5 SPDX-FileCopyrightText: 2001 Dawit Alemayehu <adawit@kde.org>
6 SPDX-FileCopyrightText: 2007, 2008 Andreas Hartmetz <ahartmetz@gmail.com>
7 SPDX-FileCopyrightText: 2008 Roland Harnau <tau@gmx.eu>
8 SPDX-FileCopyrightText: 2010 Richard Moore <rich@kde.org>
9
10 SPDX-License-Identifier: LGPL-2.0-or-later
11 */
12
13 #include "tcpslavebase.h"
14 #include "kiocoredebug.h"
15
16 #include <KConfigGroup>
17 #include <KLocalizedString>
18 #include <ksslcertificatemanager.h>
19 #include <ksslsettings.h>
20
21 #include <QSslCipher>
22 #include <QSslSocket>
23
24 #include <QDBusConnection>
25
26 using namespace KIO;
27 // using namespace KNetwork;
28
29 namespace KIO
30 {
31 Q_DECLARE_OPERATORS_FOR_FLAGS(TCPSlaveBase::SslResult)
32 }
33
34 // TODO Proxy support whichever way works; KPAC reportedly does *not* work.
35 // NOTE kded_proxyscout may or may not be interesting
36
37 // TODO resurrect SSL session recycling; this means save the session on disconnect and look
38 // for a reusable session on connect. Consider how HTTP persistent connections interact with that.
39
40 // TODO in case we support SSL-lessness we need static KTcpSocket::sslAvailable() and check it
41 // in most places we ATM check for d->isSSL.
42
43 // TODO check if d->isBlocking is honored everywhere it makes sense
44
45 // TODO fold KSSLSetting and KSSLCertificateHome into KSslSettings and use that everywhere.
46
47 // TODO recognize partially encrypted websites as "somewhat safe"
48
49 /* List of dialogs/messageboxes we need to use (current code location in parentheses)
50 - Can the "dontAskAgainName" thing be improved?
51
52 - "SSLCertDialog" [select client cert] (SlaveInterface)
53 - Enter password for client certificate (inline)
54 - Password for client cert was wrong. Please reenter. (inline)
55 - Setting client cert failed. [doesn't give reason] (inline)
56 - "SSLInfoDialog" [mostly server cert info] (SlaveInterface)
57 - You are about to enter secure mode. Security information/Display SSL information/Connect (inline)
58 - You are about to leave secure mode. Security information/Continue loading/Abort (inline)
59 - Hostname mismatch: Continue/Details/Cancel (inline)
60 - IP address mismatch: Continue/Details/Cancel (inline)
61 - Certificate failed authenticity check: Continue/Details/Cancel (inline)
62 - Would you like to accept this certificate forever: Yes/No/Current sessions only (inline)
63 */
64
65 /** @internal */
66 class Q_DECL_HIDDEN TCPSlaveBase::TcpSlaveBasePrivate
67 {
68 public:
TcpSlaveBasePrivate(TCPSlaveBase * qq)69 explicit TcpSlaveBasePrivate(TCPSlaveBase *qq)
70 : q(qq)
71 {
72 }
73
setSslMetaData()74 void setSslMetaData()
75 {
76 sslMetaData.insert(QStringLiteral("ssl_in_use"), QStringLiteral("TRUE"));
77 QSslCipher cipher = socket.sessionCipher();
78 sslMetaData.insert(QStringLiteral("ssl_protocol_version"), cipher.protocolString());
79 sslMetaData.insert(QStringLiteral("ssl_cipher"), cipher.name());
80 sslMetaData.insert(QStringLiteral("ssl_cipher_used_bits"), QString::number(cipher.usedBits()));
81 sslMetaData.insert(QStringLiteral("ssl_cipher_bits"), QString::number(cipher.supportedBits()));
82 sslMetaData.insert(QStringLiteral("ssl_peer_ip"), ip);
83
84 const QList<QSslCertificate> peerCertificateChain = socket.peerCertificateChain();
85 // try to fill in the blanks, i.e. missing certificates, and just assume that
86 // those belong to the peer (==website or similar) certificate.
87 for (int i = 0; i < sslErrors.count(); i++) {
88 if (sslErrors[i].certificate().isNull()) {
89 sslErrors[i] = QSslError(sslErrors[i].error(), peerCertificateChain[0]);
90 }
91 }
92
93 QString errorStr;
94 // encode the two-dimensional numeric error list using '\n' and '\t' as outer and inner separators
95 for (const QSslCertificate &cert : peerCertificateChain) {
96 for (const QSslError &error : std::as_const(sslErrors)) {
97 if (error.certificate() == cert) {
98 errorStr += QString::number(static_cast<int>(error.error())) + QLatin1Char('\t');
99 }
100 }
101 if (errorStr.endsWith(QLatin1Char('\t'))) {
102 errorStr.chop(1);
103 }
104 errorStr += QLatin1Char('\n');
105 }
106 errorStr.chop(1);
107 sslMetaData.insert(QStringLiteral("ssl_cert_errors"), errorStr);
108
109 QString peerCertChain;
110 for (const QSslCertificate &cert : peerCertificateChain) {
111 peerCertChain += QString::fromUtf8(cert.toPem()) + QLatin1Char('\x01');
112 }
113 peerCertChain.chop(1);
114 sslMetaData.insert(QStringLiteral("ssl_peer_chain"), peerCertChain);
115 sendSslMetaData();
116 }
117
clearSslMetaData()118 void clearSslMetaData()
119 {
120 sslMetaData.clear();
121 sslMetaData.insert(QStringLiteral("ssl_in_use"), QStringLiteral("FALSE"));
122 sendSslMetaData();
123 }
124
sendSslMetaData()125 void sendSslMetaData()
126 {
127 MetaData::ConstIterator it = sslMetaData.constBegin();
128 for (; it != sslMetaData.constEnd(); ++it) {
129 q->setMetaData(it.key(), it.value());
130 }
131 }
132
133 SslResult startTLSInternal(QSsl::SslProtocol sslVersion, int waitForEncryptedTimeout = -1);
134
135 TCPSlaveBase *const q;
136
137 bool isBlocking;
138
139 QSslSocket socket;
140
141 QString host;
142 QString ip;
143 quint16 port;
144 QByteArray serviceName;
145
146 KSSLSettings sslSettings;
147 bool usingSSL;
148 bool autoSSL;
149 bool sslNoUi; // If true, we just drop the connection silently
150 // if SSL certificate check fails in some way.
151 QList<QSslError> sslErrors;
152
153 MetaData sslMetaData;
154 };
155
156 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 83)
socket() const157 QIODevice *TCPSlaveBase::socket() const
158 {
159 return &d->socket;
160 }
161 #endif
162
tcpSocket() const163 QAbstractSocket *TCPSlaveBase::tcpSocket() const
164 {
165 return &d->socket;
166 }
167
TCPSlaveBase(const QByteArray & protocol,const QByteArray & poolSocket,const QByteArray & appSocket,bool autoSSL)168 TCPSlaveBase::TCPSlaveBase(const QByteArray &protocol, const QByteArray &poolSocket, const QByteArray &appSocket, bool autoSSL)
169 : SlaveBase(protocol, poolSocket, appSocket)
170 , d(new TcpSlaveBasePrivate(this))
171 {
172 d->isBlocking = true;
173 d->port = 0;
174 d->serviceName = protocol;
175 d->usingSSL = false;
176 d->autoSSL = autoSSL;
177 d->sslNoUi = false;
178 // Limit the read buffer size to 14 MB (14*1024*1024) (based on the upload limit
179 // in TransferJob::slotDataReq). See the docs for QAbstractSocket::setReadBufferSize
180 // and the BR# 187876 to understand why setting this limit is necessary.
181 d->socket.setReadBufferSize(14680064);
182 }
183
~TCPSlaveBase()184 TCPSlaveBase::~TCPSlaveBase()
185 {
186 delete d;
187 }
188
write(const char * data,ssize_t len)189 ssize_t TCPSlaveBase::write(const char *data, ssize_t len)
190 {
191 ssize_t written = d->socket.write(data, len);
192 if (written == -1) {
193 /*qDebug() << "d->socket.write() returned -1! Socket error is"
194 << d->socket.error() << ", Socket state is" << d->socket.state();*/
195 }
196
197 bool success = false;
198 if (d->isBlocking) {
199 // Drain the tx buffer
200 success = d->socket.waitForBytesWritten(-1);
201 } else {
202 // ### I don't know how to make sure that all data does get written at some point
203 // without doing it now. There is no event loop to do it behind the scenes.
204 // Polling in the dispatch() loop? Something timeout based?
205 success = d->socket.waitForBytesWritten(0);
206 }
207
208 d->socket.flush(); // this is supposed to get the data on the wire faster
209
210 if (d->socket.state() != QAbstractSocket::ConnectedState || !success) {
211 /*qDebug() << "Write failed, will return -1! Socket error is"
212 << d->socket.error() << ", Socket state is" << d->socket.state()
213 << "Return value of waitForBytesWritten() is" << success;*/
214 return -1;
215 }
216
217 return written;
218 }
219
read(char * data,ssize_t len)220 ssize_t TCPSlaveBase::read(char *data, ssize_t len)
221 {
222 if (d->usingSSL && (d->socket.mode() != QSslSocket::SslClientMode)) {
223 d->clearSslMetaData();
224 // qDebug() << "lost SSL connection.";
225 return -1;
226 }
227
228 if (!d->socket.bytesAvailable()) {
229 const int timeout = d->isBlocking ? -1 : (readTimeout() * 1000);
230 d->socket.waitForReadyRead(timeout);
231 }
232 return d->socket.read(data, len);
233 }
234
readLine(char * data,ssize_t len)235 ssize_t TCPSlaveBase::readLine(char *data, ssize_t len)
236 {
237 if (d->usingSSL && (d->socket.mode() != QSslSocket::SslClientMode)) {
238 d->clearSslMetaData();
239 // qDebug() << "lost SSL connection.";
240 return -1;
241 }
242
243 const int timeout = (d->isBlocking ? -1 : (readTimeout() * 1000));
244 ssize_t readTotal = 0;
245 do {
246 if (!d->socket.bytesAvailable()) {
247 d->socket.waitForReadyRead(timeout);
248 }
249 ssize_t readStep = d->socket.readLine(&data[readTotal], len - readTotal);
250 if (readStep == -1 || (readStep == 0 && d->socket.state() != QAbstractSocket::ConnectedState)) {
251 return -1;
252 }
253 readTotal += readStep;
254 } while (readTotal == 0 || data[readTotal - 1] != '\n');
255
256 return readTotal;
257 }
258
connectToHost(const QString &,const QString & host,quint16 port)259 bool TCPSlaveBase::connectToHost(const QString & /*protocol*/, const QString &host, quint16 port)
260 {
261 QString errorString;
262 const int errCode = connectToHost(host, port, &errorString);
263 if (errCode == 0) {
264 return true;
265 }
266
267 error(errCode, errorString);
268 return false;
269 }
270
connectToHost(const QString & host,quint16 port,QString * errorString)271 int TCPSlaveBase::connectToHost(const QString &host, quint16 port, QString *errorString)
272 {
273 d->clearSslMetaData(); // We have separate connection and SSL setup phases
274
275 if (errorString) {
276 errorString->clear(); // clear prior error messages.
277 }
278
279 d->socket.setPeerVerifyName(host); // Used for ssl certificate verification (SNI)
280
281 // - leaving SSL - warn before we even connect
282 //### see if it makes sense to move this into the HTTP ioslave which is the only
283 // user.
284 if (metaData(QStringLiteral("main_frame_request")) == QLatin1String("TRUE") //### this looks *really* unreliable
285 && metaData(QStringLiteral("ssl_activate_warnings")) == QLatin1String("TRUE") && metaData(QStringLiteral("ssl_was_in_use")) == QLatin1String("TRUE")
286 && !d->autoSSL) {
287 if (d->sslSettings.warnOnLeave()) {
288 int result = messageBox(i18n("You are about to leave secure "
289 "mode. Transmissions will no "
290 "longer be encrypted.\nThis "
291 "means that a third party could "
292 "observe your data in transit."),
293 WarningContinueCancel,
294 i18n("Security Information"),
295 i18n("C&ontinue Loading"),
296 QString(),
297 QStringLiteral("WarnOnLeaveSSLMode"));
298
299 if (result == SlaveBase::Cancel) {
300 if (errorString) {
301 *errorString = host;
302 }
303 return ERR_USER_CANCELED;
304 }
305 }
306 }
307
308 const int timeout = (connectTimeout() * 1000); // 20 sec timeout value
309
310 disconnectFromHost(); // Reset some state, even if we are already disconnected
311 d->host = host;
312
313 d->socket.connectToHost(host, port);
314 /*const bool connectOk = */ d->socket.waitForConnected(timeout > -1 ? timeout : -1);
315
316 /*qDebug() << "Socket: state=" << d->socket.state()
317 << ", error=" << d->socket.error()
318 << ", connected?" << connectOk;*/
319
320 if (d->socket.state() != QAbstractSocket::ConnectedState) {
321 if (errorString) {
322 *errorString = host + QLatin1String(": ") + d->socket.errorString();
323 }
324 switch (d->socket.error()) {
325 case QAbstractSocket::UnsupportedSocketOperationError:
326 return ERR_UNSUPPORTED_ACTION;
327 case QAbstractSocket::RemoteHostClosedError:
328 return ERR_CONNECTION_BROKEN;
329 case QAbstractSocket::SocketTimeoutError:
330 return ERR_SERVER_TIMEOUT;
331 case QAbstractSocket::HostNotFoundError:
332 return ERR_UNKNOWN_HOST;
333 default:
334 return ERR_CANNOT_CONNECT;
335 }
336 }
337
338 //### check for proxyAuthenticationRequiredError
339
340 d->ip = d->socket.peerAddress().toString();
341 d->port = d->socket.peerPort();
342
343 if (d->autoSSL) {
344 const SslResult res = d->startTLSInternal(QSsl::SecureProtocols, timeout);
345
346 if (res & ResultFailed) {
347 if (errorString) {
348 *errorString = i18nc("%1 is a host name", "%1: SSL negotiation failed", host);
349 }
350 return ERR_CANNOT_CONNECT;
351 }
352 }
353 return 0;
354 }
355
disconnectFromHost()356 void TCPSlaveBase::disconnectFromHost()
357 {
358 // qDebug();
359 d->host.clear();
360 d->ip.clear();
361 d->usingSSL = false;
362
363 if (d->socket.state() == QAbstractSocket::UnconnectedState) {
364 // discard incoming data - the remote host might have disconnected us in the meantime
365 // but the visible effect of disconnectFromHost() should stay the same.
366 d->socket.close();
367 return;
368 }
369
370 //### maybe save a session for reuse on SSL shutdown if and when QSslSocket
371 // does that. QCA::TLS can do it apparently but that is not enough if
372 // we want to present that as KDE API. Not a big loss in any case.
373 d->socket.disconnectFromHost();
374 if (d->socket.state() != QAbstractSocket::UnconnectedState) {
375 d->socket.waitForDisconnected(-1); // wait for unsent data to be sent
376 }
377 d->socket.close(); // whatever that means on a socket
378 }
379
isAutoSsl() const380 bool TCPSlaveBase::isAutoSsl() const
381 {
382 return d->autoSSL;
383 }
384
isUsingSsl() const385 bool TCPSlaveBase::isUsingSsl() const
386 {
387 return d->usingSSL;
388 }
389
port() const390 quint16 TCPSlaveBase::port() const
391 {
392 return d->port;
393 }
394
atEnd() const395 bool TCPSlaveBase::atEnd() const
396 {
397 return d->socket.atEnd();
398 }
399
startSsl()400 bool TCPSlaveBase::startSsl()
401 {
402 if (d->usingSSL) {
403 return false;
404 }
405 return d->startTLSInternal(QSsl::SecureProtocols) & ResultOk;
406 }
407
startTLSInternal(QSsl::SslProtocol sslVersion,int waitForEncryptedTimeout)408 TCPSlaveBase::SslResult TCPSlaveBase::TcpSlaveBasePrivate::startTLSInternal(QSsl::SslProtocol sslVersion, int waitForEncryptedTimeout)
409 {
410 // setMetaData("ssl_session_id", d->kssl->session()->toString());
411 //### we don't support session reuse for now...
412 usingSSL = true;
413
414 // Set the SSL protocol version to use...
415 socket.setProtocol(sslVersion);
416
417 /* Usually ignoreSslErrors() would be called in the slot invoked by the sslErrors()
418 signal but that would mess up the flow of control. We will check for errors
419 anyway to decide if we want to continue connecting. Otherwise ignoreSslErrors()
420 before connecting would be very insecure. */
421 socket.ignoreSslErrors();
422 socket.startClientEncryption();
423 const bool encryptionStarted = socket.waitForEncrypted(waitForEncryptedTimeout);
424
425 // Set metadata, among other things for the "SSL Details" dialog
426 QSslCipher cipher = socket.sessionCipher();
427
428 if (!encryptionStarted || socket.mode() != QSslSocket::SslClientMode || cipher.isNull() || cipher.usedBits() == 0
429 || socket.peerCertificateChain().isEmpty()) {
430 usingSSL = false;
431 clearSslMetaData();
432 /*qDebug() << "Initial SSL handshake failed. encryptionStarted is"
433 << encryptionStarted << ", cipher.isNull() is" << cipher.isNull()
434 << ", cipher.usedBits() is" << cipher.usedBits()
435 << ", length of certificate chain is" << socket.peerCertificateChain().count()
436 << ", the socket says:" << socket.errorString()
437 << "and the list of SSL errors contains"
438 << socket.sslErrors().count() << "items.";*/
439 /*for (const QSslError &sslError : socket.sslErrors()) {
440 qDebug() << "SSL ERROR: (" << sslError.error() << ")" << sslError.errorString();
441 }*/
442 return ResultFailed | ResultFailedEarly;
443 }
444
445 /*qDebug() << "Cipher info - "
446 << " advertised SSL protocol version" << socket.protocol()
447 << " negotiated SSL protocol version" << socket.sessionProtocol()
448 << " authenticationMethod:" << cipher.authenticationMethod()
449 << " encryptionMethod:" << cipher.encryptionMethod()
450 << " keyExchangeMethod:" << cipher.keyExchangeMethod()
451 << " name:" << cipher.name()
452 << " supportedBits:" << cipher.supportedBits()
453 << " usedBits:" << cipher.usedBits();*/
454
455 sslErrors = socket.sslHandshakeErrors();
456
457 // TODO: review / rewrite / remove the comment
458 // The app side needs the metadata now for the SSL error dialog (if any) but
459 // the same metadata will be needed later, too. When "later" arrives the slave
460 // may actually be connected to a different application that doesn't know
461 // the metadata the slave sent to the previous application.
462 // The quite important SSL indicator icon in Konqi's URL bar relies on metadata
463 // from here, for example. And Konqi will be the second application to connect
464 // to the slave.
465 // Therefore we choose to have our metadata and send it, too :)
466 setSslMetaData();
467 q->sendAndKeepMetaData();
468
469 SslResult rc = q->verifyServerCertificate();
470 if (rc & ResultFailed) {
471 usingSSL = false;
472 clearSslMetaData();
473 // qDebug() << "server certificate verification failed.";
474 socket.disconnectFromHost(); // Make the connection fail (cf. ignoreSslErrors())
475 return ResultFailed;
476 } else if (rc & ResultOverridden) {
477 // qDebug() << "server certificate verification failed but continuing at user's request.";
478 }
479
480 //"warn" when starting SSL/TLS
481 if (q->metaData(QStringLiteral("ssl_activate_warnings")) == QLatin1String("TRUE") && q->metaData(QStringLiteral("ssl_was_in_use")) == QLatin1String("FALSE")
482 && sslSettings.warnOnEnter()) {
483 int msgResult = q->messageBox(i18n("You are about to enter secure mode. "
484 "All transmissions will be encrypted "
485 "unless otherwise noted.\nThis means "
486 "that no third party will be able to "
487 "easily observe your data in transit."),
488 WarningYesNo,
489 i18n("Security Information"),
490 i18n("Display SSL &Information"),
491 i18n("C&onnect"),
492 QStringLiteral("WarnOnEnterSSLMode"));
493 if (msgResult == SlaveBase::Yes) {
494 q->messageBox(SSLMessageBox /*==the SSL info dialog*/, host);
495 }
496 }
497
498 return rc;
499 }
500
verifyServerCertificate()501 TCPSlaveBase::SslResult TCPSlaveBase::verifyServerCertificate()
502 {
503 d->sslNoUi = hasMetaData(QStringLiteral("ssl_no_ui")) && (metaData(QStringLiteral("ssl_no_ui")) != QLatin1String("FALSE"));
504
505 if (d->sslErrors.isEmpty()) {
506 return ResultOk;
507 } else if (d->sslNoUi) {
508 return ResultFailed;
509 }
510
511 const QList<QSslError> fatalErrors = KSslCertificateManager::nonIgnorableErrors(d->sslErrors);
512 if (!fatalErrors.isEmpty()) {
513 // TODO message "sorry, fatal error, you can't override it"
514 return ResultFailed;
515 }
516 QList<QSslCertificate> peerCertificationChain = d->socket.peerCertificateChain();
517 KSslCertificateManager *const cm = KSslCertificateManager::self();
518 KSslCertificateRule rule = cm->rule(peerCertificationChain.first(), d->host);
519
520 // remove previously seen and acknowledged errors
521 const QList<QSslError> remainingErrors = rule.filterErrors(d->sslErrors);
522 if (remainingErrors.isEmpty()) {
523 // qDebug() << "Error list empty after removing errors to be ignored. Continuing.";
524 return ResultOk | ResultOverridden;
525 }
526
527 //### We don't ask to permanently reject the certificate
528
529 QString message = i18n("The server failed the authenticity check (%1).\n\n", d->host);
530 for (const QSslError &err : std::as_const(d->sslErrors)) {
531 message += err.errorString() + QLatin1Char('\n');
532 }
533 message = message.trimmed();
534
535 int msgResult;
536 QDateTime ruleExpiry = QDateTime::currentDateTime();
537 do {
538 msgResult = messageBox(WarningYesNoCancel, message, i18n("Server Authentication"), i18n("&Details"), i18n("Co&ntinue"));
539 switch (msgResult) {
540 case SlaveBase::Yes:
541 // Details was chosen- show the certificate and error details
542 messageBox(SSLMessageBox /*the SSL info dialog*/, d->host);
543 break;
544 case SlaveBase::No: {
545 // fall through on SlaveBase::No
546 const int result = messageBox(WarningYesNoCancel,
547 i18n("Would you like to accept this "
548 "certificate forever without "
549 "being prompted?"),
550 i18n("Server Authentication"),
551 i18n("&Forever"),
552 i18n("&Current Session only"));
553 if (result == SlaveBase::Yes) {
554 // accept forever ("for a very long time")
555 ruleExpiry = ruleExpiry.addYears(1000);
556 } else if (result == SlaveBase::No) {
557 // accept "for a short time", half an hour.
558 ruleExpiry = ruleExpiry.addSecs(30 * 60);
559 } else {
560 msgResult = SlaveBase::Yes;
561 }
562 break;
563 }
564 case SlaveBase::Cancel:
565 return ResultFailed;
566 default:
567 qCWarning(KIO_CORE) << "Unexpected MessageBox response received:" << msgResult;
568 return ResultFailed;
569 }
570 } while (msgResult == SlaveBase::Yes);
571
572 // TODO special cases for wildcard domain name in the certificate!
573 // rule = KSslCertificateRule(d->socket.peerCertificateChain().first(), whatever);
574
575 rule.setExpiryDateTime(ruleExpiry);
576 rule.setIgnoredErrors(d->sslErrors);
577 cm->setRule(rule);
578
579 return ResultOk | ResultOverridden;
580 }
581
isConnected() const582 bool TCPSlaveBase::isConnected() const
583 {
584 // QSslSocket::isValid() is shady...
585 return d->socket.state() == QAbstractSocket::ConnectedState;
586 }
587
waitForResponse(int t)588 bool TCPSlaveBase::waitForResponse(int t)
589 {
590 if (d->socket.bytesAvailable()) {
591 return true;
592 }
593 return d->socket.waitForReadyRead(t * 1000);
594 }
595
setBlocking(bool b)596 void TCPSlaveBase::setBlocking(bool b)
597 {
598 if (!b) {
599 qCWarning(KIO_CORE) << "Caller requested non-blocking mode, but that doesn't work";
600 return;
601 }
602 d->isBlocking = b;
603 }
604
virtual_hook(int id,void * data)605 void TCPSlaveBase::virtual_hook(int id, void *data)
606 {
607 if (id == SlaveBase::AppConnectionMade) {
608 d->sendSslMetaData();
609 } else {
610 SlaveBase::virtual_hook(id, data);
611 }
612 }
613