kde-playground/kdepimlibs/mailtransport/servertest.cpp
2015-04-14 21:49:29 +00:00

620 lines
19 KiB
C++

/*
Copyright (c) 2006 - 2007 Volker Krause <vkrause@kde.org>
Copyright (C) 2007 KovoKs <info@kovoks.nl>
Copyright (c) 2008 Thomas McGuire <thomas.mcguire@gmx.net>
This library is free software; you can redistribute it and/or modify it
under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This library is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to the
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301, USA.
*/
// Own
#include "servertest.h"
#include "socket.h"
#include <mailtransport/transportbase.h>
#include <mailtransport/mailtransport_defs.h>
// Qt
#include <QHostInfo>
#include <QProgressBar>
#include <QTimer>
// KDE
#include <KDebug>
using namespace MailTransport;
namespace MailTransport {
class ServerTestPrivate
{
public:
ServerTestPrivate( ServerTest *test );
ServerTest *const q;
QString server;
QString fakeHostname;
QString testProtocol;
MailTransport::Socket *normalSocket;
MailTransport::Socket *secureSocket;
QSet< int > connectionResults;
QHash< int, QList<int> > authenticationResults;
QSet< ServerTest::Capability > capabilityResults;
QHash< int, uint > customPorts;
QTimer *normalSocketTimer;
QTimer *secureSocketTimer;
QTimer *progressTimer;
QProgressBar *testProgress;
bool secureSocketFinished;
bool normalSocketFinished;
bool tlsFinished;
bool popSupportsTLS;
int normalStage;
int secureStage;
int encryptionMode;
bool normalPossible;
bool securePossible;
void finalResult();
void handleSMTPIMAPResponse( int type, const QString &text );
void sendInitialCapabilityQuery( MailTransport::Socket *socket );
bool handlePopConversation( MailTransport::Socket *socket, int type, int stage,
const QString &response, bool *shouldStartTLS );
QList< int > parseAuthenticationList( const QStringList &authentications );
// slots
void slotNormalPossible();
void slotNormalNotPossible();
void slotSslPossible();
void slotSslNotPossible();
void slotTlsDone();
void slotReadNormal( const QString &text );
void slotReadSecure( const QString &text );
void slotUpdateProgress();
};
}
ServerTestPrivate::ServerTestPrivate( ServerTest *test )
: q( test ), testProgress( 0 ), secureSocketFinished( false ),
normalSocketFinished( false ), tlsFinished( false ),
normalPossible( true ), securePossible( true )
{
}
void ServerTestPrivate::finalResult()
{
if ( !secureSocketFinished || !normalSocketFinished || !tlsFinished ) {
return;
}
kDebug() << "Modes:" << connectionResults;
kDebug() << "Capabilities:" << capabilityResults;
kDebug() << "Normal:" << q->normalProtocols();
kDebug() << "SSL:" << q->secureProtocols();
kDebug() << "TLS:" << q->tlsProtocols();
if ( testProgress ) {
testProgress->hide();
}
progressTimer->stop();
secureSocketFinished = false;
normalSocketFinished = false;
tlsFinished = false ;
emit q->finished( connectionResults.toList() );
}
QList< int > ServerTestPrivate::parseAuthenticationList( const QStringList &authentications )
{
QList< int > result;
for ( QStringList::ConstIterator it = authentications.begin();
it != authentications.end(); ++it ) {
QString current = ( *it ).toUpper();
if ( current == QLatin1String( "LOGIN" ) ) {
result << Transport::EnumAuthenticationType::LOGIN;
} else if ( current == QLatin1String( "PLAIN" ) ) {
result << Transport::EnumAuthenticationType::PLAIN;
} else if ( current == QLatin1String( "CRAM-MD5" ) ) {
result << Transport::EnumAuthenticationType::CRAM_MD5;
} else if ( current == QLatin1String( "DIGEST-MD5" ) ) {
result << Transport::EnumAuthenticationType::DIGEST_MD5;
} else if ( current == QLatin1String( "NTLM" ) ) {
result << Transport::EnumAuthenticationType::NTLM;
} else if ( current == QLatin1String( "GSSAPI" ) ) {
result << Transport::EnumAuthenticationType::GSSAPI;
} else if ( current == QLatin1String( "ANONYMOUS" ) ) {
result << Transport::EnumAuthenticationType::ANONYMOUS;
}
// APOP is handled by handlePopConversation()
}
kDebug() << authentications << result;
// LOGIN doesn't offer anything over PLAIN, requires more server
// roundtrips and is not an official SASL mechanism, but a MS-ism,
// so only enable it if PLAIN isn't available:
if ( result.contains( Transport::EnumAuthenticationType::PLAIN ) ) {
result.removeAll( Transport::EnumAuthenticationType::LOGIN );
}
return result;
}
void ServerTestPrivate::handleSMTPIMAPResponse( int type, const QString &text )
{
if ( !text.contains( QLatin1String( "AUTH" ), Qt::CaseInsensitive ) ) {
kDebug() << "No authentication possible";
return;
}
QStringList protocols;
protocols << QLatin1String( "LOGIN" ) << QLatin1String( "PLAIN" )
<< QLatin1String( "CRAM-MD5" ) << QLatin1String( "DIGEST-MD5" )
<< QLatin1String( "NTLM" ) << QLatin1String( "GSSAPI" )
<< QLatin1String( "ANONYMOUS" );
QStringList results;
for ( int i = 0; i < protocols.count(); ++i ) {
if ( text.contains( protocols.at( i ), Qt::CaseInsensitive ) ) {
results.append( protocols.at( i ) );
}
}
authenticationResults[type] = parseAuthenticationList( results );
// if we couldn't parse any authentication modes, default to clear-text
if ( authenticationResults[type].size() == 0 ) {
authenticationResults[type] << Transport::EnumAuthenticationType::CLEAR;
}
kDebug() << "For type" << type << ", we have:" << authenticationResults[type];
}
void ServerTestPrivate::slotNormalPossible()
{
normalSocketTimer->stop();
connectionResults << Transport::EnumEncryption::None;
}
void ServerTestPrivate::sendInitialCapabilityQuery( MailTransport::Socket *socket )
{
if ( testProtocol == IMAP_PROTOCOL ) {
socket->write( QLatin1String( "1 CAPABILITY" ) );
} else if ( testProtocol == SMTP_PROTOCOL ) {
// Detect the hostname which we send with the EHLO command.
// If there is a fake one set, use that, otherwise use the
// local host name (and make sure it contains a domain, so the
// server thinks it is valid).
QString hostname;
if ( !fakeHostname.isNull() ) {
hostname = fakeHostname;
} else {
hostname = QHostInfo::localHostName();
if ( hostname.isEmpty() ) {
hostname = QLatin1String( "localhost.invalid" );
} else if ( !hostname.contains( QChar::fromLatin1( '.' ) ) ) {
hostname += QLatin1String( ".localnet" );
}
}
kDebug() << "Hostname for EHLO is" << hostname;
socket->write( QLatin1String( "EHLO " ) + hostname );
}
}
void ServerTestPrivate::slotTlsDone()
{
// The server will not send a response after starting TLS. Therefore, we have to manually
// call slotReadNormal(), because this is not triggered by a data received signal this time.
slotReadNormal( QString() );
}
bool ServerTestPrivate::handlePopConversation( MailTransport::Socket *socket, int type, int stage,
const QString &response, bool *shouldStartTLS )
{
Q_ASSERT( shouldStartTLS != 0 );
// Initial Greeting
if ( stage == 0 ) {
//Regexp taken from POP3 ioslave
QString responseWithoutCRLF = response;
responseWithoutCRLF.chop( 2 );
QRegExp re( QLatin1String( "<[A-Za-z0-9\\.\\-_]+@[A-Za-z0-9\\.\\-_]+>$" ),
Qt::CaseInsensitive );
if ( responseWithoutCRLF.indexOf( re ) != -1 ) {
authenticationResults[type] << Transport::EnumAuthenticationType::APOP;
}
//Each server is supposed to support clear text login
authenticationResults[type] << Transport::EnumAuthenticationType::CLEAR;
// If we are in TLS stage, the server does not send the initial greeting.
// Assume that the APOP availability is the same as with an unsecured connection.
if ( type == Transport::EnumEncryption::TLS &&
authenticationResults[Transport::EnumEncryption::None].
contains( Transport::EnumAuthenticationType::APOP ) ) {
authenticationResults[Transport::EnumEncryption::TLS]
<< Transport::EnumAuthenticationType::APOP;
}
socket->write( QLatin1String( "CAPA" ) );
return true;
}
// CAPA result
else if ( stage == 1 ) {
// Example:
// CAPA
// +OK
// TOP
// USER
// SASL LOGIN CRAM-MD5
// UIDL
// RESP-CODES
// .
if ( response.contains( QLatin1String( "TOP" ) ) ) {
capabilityResults += ServerTest::Top;
}
if ( response.contains( QLatin1String( "PIPELINING" ) ) ) {
capabilityResults += ServerTest::Pipelining;
}
if ( response.contains( QLatin1String( "UIDL" ) ) ) {
capabilityResults += ServerTest::UIDL;
}
if ( response.contains( QLatin1String( "STLS" ) ) ) {
connectionResults << Transport::EnumEncryption::TLS;
popSupportsTLS = true;
}
socket->write( QLatin1String( "AUTH" ) );
return true;
}
// AUTH response
else if ( stage == 2 ) {
// Example:
// C: AUTH
// S: +OK List of supported authentication methods follows
// S: LOGIN
// S: CRAM-MD5
// S:.
QString formattedReply = response;
// Get rid of trailling ".CRLF"
formattedReply.chop( 3 );
// Get rid of the first +OK line
formattedReply = formattedReply.right( formattedReply.size() -
formattedReply.indexOf( QLatin1Char( '\n' ) ) - 1 );
formattedReply =
formattedReply.replace( QLatin1Char( ' ' ), QLatin1Char( '-' ) ).
replace( QLatin1String( "\r\n" ), QLatin1String( " " ) );
authenticationResults[type] +=
parseAuthenticationList( formattedReply.split( QLatin1Char( ' ' ) ) );
}
*shouldStartTLS = popSupportsTLS;
return false;
}
// slotReadNormal() handles normal (no) encryption and TLS encryption.
// At first, the communication is not encrypted, but if the server supports
// the STARTTLS/STLS keyword, the same authentication query is done again
// with TLS.
void ServerTestPrivate::slotReadNormal( const QString &text )
{
Q_ASSERT( encryptionMode != Transport::EnumEncryption::SSL );
static const int tlsHandshakeStage = 42;
kDebug() << "Stage" << normalStage + 1 << ", Mode" << encryptionMode;
// If we are in stage 42, we just do the handshake for TLS encryption and
// then reset the stage to -1, so that all authentication modes and
// capabilities are queried again for TLS encryption (some servers have
// different authentication methods in normal and in TLS mode).
if ( normalStage == tlsHandshakeStage ) {
Q_ASSERT( encryptionMode == Transport::EnumEncryption::TLS );
normalStage = -1;
normalSocket->startTLS();
return;
}
bool shouldStartTLS = false;
normalStage++;
// Handle the whole POP converstation separatly, it is very different from
// IMAP and SMTP
if ( testProtocol == POP_PROTOCOL ) {
if ( handlePopConversation( normalSocket, encryptionMode, normalStage, text,
&shouldStartTLS ) ) {
return;
}
} else {
// Handle the SMTP/IMAP conversation here. We just send the EHLO command in
// sendInitialCapabilityQuery.
if ( normalStage == 0 ) {
sendInitialCapabilityQuery( normalSocket );
return;
}
if ( text.contains( QLatin1String( "STARTTLS" ), Qt::CaseInsensitive ) ) {
connectionResults << Transport::EnumEncryption::TLS;
shouldStartTLS = true;
}
handleSMTPIMAPResponse( encryptionMode, text );
}
// If we reach here, the normal authentication/capabilities query is completed.
// Now do the same for TLS.
normalSocketFinished = true;
// If the server announced that STARTTLS/STLS is available, we'll add TLS to the
// connection result, do the command and set the stage to 42 to start the handshake.
if ( shouldStartTLS && encryptionMode == Transport::EnumEncryption::None ) {
kDebug() << "Trying TLS...";
connectionResults << Transport::EnumEncryption::TLS;
if ( testProtocol == POP_PROTOCOL ) {
normalSocket->write( QLatin1String( "STLS" ) );
} else if ( testProtocol == IMAP_PROTOCOL ) {
normalSocket->write( QLatin1String( "2 STARTTLS" ) );
} else {
normalSocket->write( QLatin1String( "STARTTLS" ) );
}
encryptionMode = Transport::EnumEncryption::TLS;
normalStage = tlsHandshakeStage;
return;
}
// If we reach here, either the TLS authentication/capabilities query is finished
// or the server does not support the STARTTLS/STLS command.
tlsFinished = true;
finalResult();
}
void ServerTestPrivate::slotReadSecure( const QString &text )
{
secureStage++;
if ( testProtocol == POP_PROTOCOL ) {
bool dummy;
if ( handlePopConversation( secureSocket, Transport::EnumEncryption::SSL,
secureStage, text, &dummy ) ) {
return;
}
} else {
if ( secureStage == 0 ) {
sendInitialCapabilityQuery( secureSocket );
return;
}
handleSMTPIMAPResponse( Transport::EnumEncryption::SSL, text );
}
secureSocketFinished = true;
finalResult();
}
void ServerTestPrivate::slotNormalNotPossible()
{
normalSocketTimer->stop();
normalPossible = false;
normalSocketFinished = true;
tlsFinished = true;
finalResult();
}
void ServerTestPrivate::slotSslPossible()
{
secureSocketTimer->stop();
connectionResults << Transport::EnumEncryption::SSL;
}
void ServerTestPrivate::slotSslNotPossible()
{
secureSocketTimer->stop();
securePossible = false;
secureSocketFinished = true;
finalResult();
}
void ServerTestPrivate::slotUpdateProgress()
{
if ( testProgress ) {
testProgress->setValue( testProgress->value() + 1 );
}
}
//---------------------- end private class -----------------------//
ServerTest::ServerTest( QWidget *parent )
: QWidget( parent ), d( new ServerTestPrivate( this ) )
{
d->normalSocketTimer = new QTimer( this );
d->normalSocketTimer->setSingleShot( true );
connect( d->normalSocketTimer, SIGNAL(timeout()), SLOT(slotNormalNotPossible()) );
d->secureSocketTimer = new QTimer( this );
d->secureSocketTimer->setSingleShot( true );
connect( d->secureSocketTimer, SIGNAL(timeout()), SLOT(slotSslNotPossible()) );
d->progressTimer = new QTimer( this );
connect( d->progressTimer, SIGNAL(timeout()), SLOT(slotUpdateProgress()) );
}
ServerTest::~ServerTest()
{
delete d;
}
void ServerTest::start()
{
kDebug() << d;
d->connectionResults.clear();
d->authenticationResults.clear();
d->capabilityResults.clear();
d->popSupportsTLS = false;
d->normalStage = -1;
d->secureStage = -1;
d->encryptionMode = Transport::EnumEncryption::None;
d->normalPossible = true;
d->securePossible = true;
if ( d->testProgress ) {
d->testProgress->setMaximum( 20 );
d->testProgress->setValue( 0 );
d->testProgress->setTextVisible( true );
d->testProgress->show();
d->progressTimer->start( 1000 );
}
d->normalSocket = new MailTransport::Socket( this );
d->secureSocket = new MailTransport::Socket( this );
d->normalSocket->setObjectName( QLatin1String( "normal" ) );
d->normalSocket->setServer( d->server );
d->normalSocket->setProtocol( d->testProtocol );
if ( d->testProtocol == IMAP_PROTOCOL ) {
d->normalSocket->setPort( IMAP_PORT );
d->secureSocket->setPort( IMAPS_PORT );
} else if ( d->testProtocol == SMTP_PROTOCOL ) {
d->normalSocket->setPort( SMTP_PORT );
d->secureSocket->setPort( SMTPS_PORT );
} else if ( d->testProtocol == POP_PROTOCOL ) {
d->normalSocket->setPort( POP_PORT );
d->secureSocket->setPort( POPS_PORT );
}
if ( d->customPorts.contains( Transport::EnumEncryption::None ) ) {
d->normalSocket->setPort( d->customPorts.value( Transport::EnumEncryption::None ) );
}
if ( d->customPorts.contains( Transport::EnumEncryption::SSL ) ) {
d->secureSocket->setPort( d->customPorts.value( Transport::EnumEncryption::SSL ) );
}
connect( d->normalSocket, SIGNAL(connected()), SLOT(slotNormalPossible()) );
connect( d->normalSocket, SIGNAL(failed()), SLOT(slotNormalNotPossible()) );
connect( d->normalSocket, SIGNAL(data(QString)),
SLOT(slotReadNormal(QString)) );
connect( d->normalSocket, SIGNAL(tlsDone()), SLOT(slotTlsDone()));
d->normalSocket->reconnect();
d->normalSocketTimer->start( 10000 );
d->secureSocket->setObjectName( QLatin1String( "secure" ) );
d->secureSocket->setServer( d->server );
d->secureSocket->setProtocol( d->testProtocol + QLatin1Char( 's' ) );
d->secureSocket->setSecure( true );
connect( d->secureSocket, SIGNAL(connected()), SLOT(slotSslPossible()) );
connect( d->secureSocket, SIGNAL(failed()), SLOT(slotSslNotPossible()) );
connect( d->secureSocket, SIGNAL(data(QString)),
SLOT(slotReadSecure(QString)) );
d->secureSocket->reconnect();
d->secureSocketTimer->start( 10000 );
}
void ServerTest::setFakeHostname( const QString &fakeHostname )
{
d->fakeHostname = fakeHostname;
}
QString ServerTest::fakeHostname()
{
return d->fakeHostname;
}
void ServerTest::setServer( const QString &server )
{
d->server = server;
}
void ServerTest::setPort( Transport::EnumEncryption::type encryptionMode, uint port )
{
Q_ASSERT( encryptionMode == Transport::EnumEncryption::None ||
encryptionMode == Transport::EnumEncryption::SSL );
d->customPorts.insert( encryptionMode, port );
}
void ServerTest::setProgressBar( QProgressBar *pb )
{
d->testProgress = pb;
}
void ServerTest::setProtocol( const QString &protocol )
{
d->testProtocol = protocol;
}
QString ServerTest::protocol()
{
return d->testProtocol;
}
QString ServerTest::server()
{
return d->server;
}
int ServerTest::port( Transport::EnumEncryption::type encryptionMode )
{
Q_ASSERT( encryptionMode == Transport::EnumEncryption::None ||
encryptionMode == Transport::EnumEncryption::SSL );
if ( d->customPorts.contains( encryptionMode ) ) {
return d->customPorts.value( static_cast<int>( encryptionMode ) );
} else {
return -1;
}
}
QProgressBar *ServerTest::progressBar()
{
return d->testProgress;
}
QList< int > ServerTest::normalProtocols()
{
return d->authenticationResults[TransportBase::EnumEncryption::None];
}
bool ServerTest::isNormalPossible()
{
return d->normalPossible;
}
QList< int > ServerTest::tlsProtocols()
{
return d->authenticationResults[TransportBase::EnumEncryption::TLS];
}
QList< int > ServerTest::secureProtocols()
{
return d->authenticationResults[Transport::EnumEncryption::SSL];
}
bool ServerTest::isSecurePossible()
{
return d->securePossible;
}
QList< ServerTest::Capability > ServerTest::capabilities() const
{
return d->capabilityResults.toList();
}
#include "moc_servertest.cpp"