Документация
ОС Аврора 5.0.1
saslclient.cpp
В приведённом ниже коде показано, как создать SASL-клиент.
/*
Copyright (C) 2003-2008 Justin Karneges <justin@affinix.com>
Copyright (C) 2006 Michail Pishchagin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <QCoreApplication>
#include <QTcpServer>
#include <QTcpSocket>
#include <QTimer>
#include <cstdio>
// QtCrypto содержит объявления всех сущностей QCA.
#include <QtCrypto>
#ifdef QT_STATICPLUGIN
#include "import_plugins.h"
#endif
static QString prompt(const QString &s)
{
printf("* %s ", qPrintable(s));
fflush(stdout);
char line[256];
fgets(line, 255, stdin);
QString result = QString::fromLatin1(line);
if (result[result.length() - 1] == QLatin1Char('\n'))
result.truncate(result.length() - 1);
return result;
}
static QString socketErrorToString(QAbstractSocket::SocketError x)
{
QString s;
switch (x) {
case QAbstractSocket::ConnectionRefusedError:
s = QStringLiteral("в соединении отказано или истекло время ожидания");
break;
case QAbstractSocket::RemoteHostClosedError:
s = QStringLiteral("удалённый хост закрыл соединение");
break;
case QAbstractSocket::HostNotFoundError:
s = QStringLiteral("хост не найден");
break;
case QAbstractSocket::SocketAccessError:
s = QStringLiteral("ошибка доступа");
break;
case QAbstractSocket::SocketResourceError:
s = QStringLiteral("слишком много сокетов");
break;
case QAbstractSocket::SocketTimeoutError:
s = QStringLiteral("время ожидания операции истекло");
break;
case QAbstractSocket::DatagramTooLargeError:
s = QStringLiteral("размер датаграммы превышает системный предел");
break;
case QAbstractSocket::NetworkError:
s = QStringLiteral("ошибка сети");
break;
case QAbstractSocket::AddressInUseError:
s = QStringLiteral("адрес уже используется");
break;
case QAbstractSocket::SocketAddressNotAvailableError:
s = QStringLiteral("адрес не принадлежит хосту");
break;
case QAbstractSocket::UnsupportedSocketOperationError:
s = QStringLiteral("операция не поддерживается локальной операционной системой");
break;
default:
s = QStringLiteral("неизвестная ошибка сокета");
break;
}
return s;
}
static QString saslAuthConditionToString(QCA::SASL::AuthCondition x)
{
QString s;
switch (x) {
case QCA::SASL::NoMechanism:
s = QStringLiteral("не удалось согласовать подходящий механизм");
break;
case QCA::SASL::BadProtocol:
s = QStringLiteral("недействительный протокол SASL");
break;
case QCA::SASL::BadServer:
s = QStringLiteral("сбой взаимной аутентификации сервера");
break;
// AuthFail или неизвестная ошибка (включая те, которые определены только для сервера).
default:
s = QStringLiteral("общая ошибка аутентификации");
break;
};
return s;
}
class ClientTest : public QObject
{
Q_OBJECT
private:
QString host, proto, authzid, realm, user, pass;
int port;
bool no_authzid, no_realm;
int mode; // 0 = получение списка механизмов, 1 = согласование SASL, 2 = приложение.
QTcpSocket *sock;
QCA::SASL * sasl;
QByteArray inbuf;
bool sock_done;
int waitCycles;
public:
ClientTest(const QString &_host,
int _port,
const QString &_proto,
const QString &_authzid,
const QString &_realm,
const QString &_user,
const QString &_pass,
bool _no_authzid,
bool _no_realm)
: host(_host)
, proto(_proto)
, authzid(_authzid)
, realm(_realm)
, user(_user)
, pass(_pass)
, port(_port)
, no_authzid(_no_authzid)
, no_realm(_no_realm)
, sock_done(false)
, waitCycles(0)
{
sock = new QTcpSocket(this);
connect(sock, &QTcpSocket::connected, this, &ClientTest::sock_connected);
connect(sock, &QTcpSocket::readyRead, this, &ClientTest::sock_readyRead);
connect(sock, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this, &ClientTest::sock_error);
sasl = new QCA::SASL(this);
connect(sasl, &QCA::SASL::clientStarted, this, &ClientTest::sasl_clientFirstStep);
connect(sasl, &QCA::SASL::nextStep, this, &ClientTest::sasl_nextStep);
connect(sasl, &QCA::SASL::needParams, this, &ClientTest::sasl_needParams);
connect(sasl, &QCA::SASL::authenticated, this, &ClientTest::sasl_authenticated);
connect(sasl, &QCA::SASL::readyRead, this, &ClientTest::sasl_readyRead);
connect(sasl, &QCA::SASL::readyReadOutgoing, this, &ClientTest::sasl_readyReadOutgoing);
connect(sasl, &QCA::SASL::error, this, &ClientTest::sasl_error);
}
public Q_SLOTS:
void start()
{
mode = 0; // Режим списка механизмов.
int flags = 0;
flags |= QCA::SASL::AllowPlain;
flags |= QCA::SASL::AllowAnonymous;
sasl->setConstraints((QCA::SASL::AuthFlags)flags, 0, 256);
if (!user.isEmpty())
sasl->setUsername(user);
if (!authzid.isEmpty())
sasl->setAuthzid(authzid);
if (!pass.isEmpty())
sasl->setPassword(pass.toUtf8());
if (!realm.isEmpty())
sasl->setRealm(realm);
printf("Подключение к %s:%d, для протокола %s\n", qPrintable(host), port, qPrintable(proto));
sock->connectToHost(host, port);
}
Q_SIGNALS:
void quit();
private Q_SLOTS:
void sock_connected()
{
printf("Подключено к серверу. Ожидание списка механизмов…\n");
}
void sock_error(QAbstractSocket::SocketError x)
{
if (x == QAbstractSocket::RemoteHostClosedError) {
if (mode == 2) // Режим приложения, где отключение означает завершение.
{
sock_done = true;
tryFinished();
return 0;
} else // Любой другой режим, где отключение — это ошибка.
{
printf("Ошибка: сервер неожиданно закрыл соединение.\n");
emit quit();
return 0;
}
}
printf("Ошибка: сокет = %s\n", qPrintable(socketErrorToString(x)));
emit quit();
}
void sock_readyRead()
{
if (mode == 2) // Режим приложения.
{
QByteArray a = sock->readAll();
printf("Прочитать %d байт\n", a.size());
// Возможный недостаток QCA API 2.0 состоит в том,
// что если данные SASL получены от однорангового узла
// с последующим отключением от однорангового узла,
// нет чёткого подхода к восстановлению байтов, которые могут быть потеряны при прерывании/закрытии соединения. Для TLS
// этой проблемы не возникает, потому что TLS имеет концепцию
// закрытия сеанса. У SASL нет такой концепции работы с завершением сеанса,
// и поскольку QCA API является асинхронным,
// потенциально алгоритм мог бы вечно ожидать декодированные
// данные, если бы последняя запись содержала бы только часть данных.
//
// На текущий момент можно применить простой обходной путь.
// Нужно подождать по крайней мере три цикла обработки событий для
// декодированных данных, прежде чем закончить работу, предположив,
// что последняя запись была частичной. Дело в том, что все текущие
// криптопровайдеры QCA SASL отвечают в течение такого периода времени,
// так что подобный подход должен хорошо работать.
// В QCA 2.1 необходимо пересмотреть API, чтобы
// лучше справляться с этой проблемой.
//
// Следующее примечание: предполагается, что такая особенность влияет только на
// протоколы приложений, которые не отправляют собственное сообщение
// о закрытии и полагаются на закрытие на уровне TCP. Примерами
// являются HTTP и, конечно же, протокол QCATest.
if (waitCycles == 0) {
waitCycles = 3;
QMetaObject::invokeMethod(this, "waitWriteIncoming", Qt::QueuedConnection);
}
sasl->writeIncoming(a);
} else // Список механизмов или режим согласования SASL.
{
if (sock->canReadLine()) {
QString line = QString::fromLatin1(sock->readLine());
line.truncate(line.length() - 1); // Обрезается новая строка.
handleLine(line);
}
}
}
void sasl_clientFirstStep(bool clientInit, const QByteArray &clientInitData)
{
printf("Выбор механизма: %s\n", qPrintable(sasl->mechanism()));
QString line = sasl->mechanism();
if (clientInit) {
line += QLatin1Char(' ');
line += arrayToString(clientInitData);
}
sendLine(line);
}
void sasl_nextStep(const QByteArray &stepData)
{
QString line = QStringLiteral("C");
if (!stepData.isEmpty()) {
line += QLatin1Char(',');
line += arrayToString(stepData);
}
sendLine(line);
}
void sasl_needParams(const QCA::SASL::Params ¶ms)
{
if (params.needUsername()) {
user = prompt(QStringLiteral("Имя пользователя:"));
sasl->setUsername(user);
}
if (params.canSendAuthzid() && !no_authzid) {
authzid = prompt(QStringLiteral("Авторизоваться как (нажмите enter, чтобы пропустить шаг):"));
if (!authzid.isEmpty())
sasl->setAuthzid(authzid);
}
if (params.needPassword()) {
QCA::ConsolePrompt prompt;
prompt.getHidden(QStringLiteral("* Пароль"));
prompt.waitForFinished();
QCA::SecureArray pass = prompt.result();
sasl->setPassword(pass);
}
if (params.canSendRealm() && !no_realm) {
QStringList realms = sasl->realmList();
printf("Доступные области:\n");
if (realms.isEmpty())
printf(" (не указано)\n");
foreach (const QString &s, realms)
printf(" %s\n", qPrintable(s));
realm = prompt(QStringLiteral("Область (нажмите enter, чтобы пропустить шаг):"));
if (!realm.isEmpty())
sasl->setRealm(realm);
}
sasl->continueAfterParams();
}
void sasl_authenticated()
{
printf("Успешная аутентификация SASL!\n");
printf("SSF: %d\n", sasl->ssf());
}
void sasl_readyRead()
{
QByteArray a = sasl->read();
inbuf += a;
processInbuf();
}
void sasl_readyReadOutgoing()
{
QByteArray a = sasl->readOutgoing();
sock->write(a);
}
void sasl_error()
{
int e = sasl->errorCode();
if (e == QCA::SASL::ErrorInit)
printf("Ошибка SASL: ошибка инициализации.\n");
else if (e == QCA::SASL::ErrorHandshake)
printf("Ошибка SASL: %s.\n", qPrintable(saslAuthConditionToString(sasl->authCondition())));
else if (e == QCA::SASL::ErrorCrypt)
printf("Ошибка SASL: нарушен уровень безопасности.\n");
else
printf("Ошибка SASL: неизвестная ошибка.\n");
emit quit();
}
void waitWriteIncoming()
{
--waitCycles;
if (waitCycles > 0) {
QMetaObject::invokeMethod(this, "waitWriteIncoming", Qt::QueuedConnection);
return 0;
}
tryFinished();
}
private:
void tryFinished()
{
if (sock_done && waitCycles == 0) {
printf("Завершено, соединение с сервером закрыто.\n");
// Если не реализовывать ожидание ответа от
// writeIncoming, то он может прийти слишком поздно.
// Теоретически это не должно случиться, если приложение ждёт в течение
// достаточного количества циклов, но иногда ответ может
// вернуться после запроса на выход из приложения,
// но до фактического выхода из приложения. Чтобы упростить
// отладку, приложение явно перестаёт здесь
// обрабатывать сигналы. В противном случае ответ всё равно
// может быть получен и отображён, что создаст ложное
// ощущение правильности работы.
sasl->disconnect(this);
emit quit();
}
}
QString arrayToString(const QByteArray &ba)
{
return QCA::Base64().arrayToString(ba);
}
QByteArray stringToArray(const QString &s)
{
return QCA::Base64().stringToArray(s).toByteArray();
}
void sendLine(const QString &line)
{
printf("Запись: {%s}\n", qPrintable(line));
QString s = line + QLatin1Char('\n');
QByteArray a = s.toUtf8();
if (mode == 2) // Режим приложения.
sasl->write(a); // Запись в SASL.
else // Список механизмов или согласование SASL.
sock->write(a); // Запись в сокет.
}
void processInbuf()
{
// Готовые строки собираются из inbuf.
QStringList list;
int at;
while ((at = inbuf.indexOf('\n')) != -1) {
list += QString::fromUtf8(inbuf.mid(0, at));
inbuf = inbuf.mid(at + 1);
}
// Обработка строк.
foreach (const QString &line, list)
handleLine(line);
}
void handleLine(const QString &line)
{
printf("Чтение: [%s]\n", qPrintable(line));
if (mode == 0) {
// Первая строка — это список методов.
const QStringList mechlist = line.split(QLatin1Char(' '));
mode = 1; // Переключение в режим согласования SASL.
sasl->startClient(proto, host, mechlist);
} else if (mode == 1) {
QString type, rest;
int n = line.indexOf(QLatin1Char(','));
if (n != -1) {
type = line.mid(0, n);
rest = line.mid(n + 1);
} else
type = line;
if (type == QLatin1String("C")) {
sasl->putStep(stringToArray(rest));
} else if (type == QLatin1String("E")) {
if (!rest.isEmpty())
printf("Ошибка. Сервер сообщает: %s.\n", qPrintable(rest));
else
printf("Неизвестная ошибка сервера.\n");
emit quit();
return 0;
} else if (type == QLatin1String("A")) {
printf("Успешная аутентификация.\n");
mode = 2; // Переключение в режим приложения.
// В этот момент сервер может отправить текстовые
// строки для отображения, а затем закрыть соединение.
sock_readyRead(); // Какие-нибудь дополнительные данные?
return 0;
} else {
printf("Ошибка: недопустимый формат данных. Соединение закрывается.\n");
emit quit();
return 0;
}
}
}
};
void usage()
{
printf("использование: saslclient (options) host(:port) (user) (pass)\n");
printf("опции: --proto=x, --authzid=x, --realm=x\n");
}
int main(int argc, char **argv)
{
QCA::Initializer init;
QCoreApplication qapp(argc, argv);
QStringList args = qapp.arguments();
args.removeFirst();
// Опции.
QString proto = QStringLiteral("qcatest"); // Протокол по умолчанию.
QString authzid, realm;
bool no_authzid = false;
bool no_realm = false;
for (int n = 0; n < args.count(); ++n) {
if (!args[n].startsWith(QLatin1String("--")))
continue;
QString opt = args[n].mid(2);
QString var, val;
int at = opt.indexOf(QLatin1Char('='));
if (at != -1) {
var = opt.mid(0, at);
val = opt.mid(at + 1);
} else
var = opt;
if (var == QLatin1String("proto")) {
proto = val;
} else if (var == QLatin1String("authzid")) {
// Указание пустого authzid означает, что данные намеренно не указаны.
if (val.isEmpty())
no_authzid = true;
else
authzid = val;
} else if (var == QLatin1String("realm")) {
// Указание пустого realm означает, что данные намеренно не указаны.
if (val.isEmpty())
no_realm = true;
else
realm = val;
}
args.removeAt(n);
--n; // Смещение позиции.
}
if (args.count() < 1) {
usage();
return 0;
}
QString host, user, pass;
int port = 8001; // Порт по умолчанию.
QString hostinput = args[0];
if (args.count() >= 2)
user = args[1];
if (args.count() >= 3)
pass = args[2];
int at = hostinput.indexOf(QLatin1Char(':'));
if (at != -1) {
host = hostinput.mid(0, at);
port = hostinput.midRef(at + 1).toInt();
} else
host = hostinput;
if (!QCA::isSupported("sasl")) {
printf("Ошибка: поддержка SASL не найдена.\n");
return 1;
}
ClientTest client(host, port, proto, authzid, realm, user, pass, no_authzid, no_realm);
QObject::connect(&client, &ClientTest::quit, &qapp, &QCoreApplication::quit);
QTimer::singleShot(0, &client, &ClientTest::start);
qapp.exec();
return 0;
}
#include "saslclient.moc"