1/*
2 Copyright (C) 2003-2008 Justin Karneges <justin@affinix.com>
3 Copyright (C) 2006 Michail Pishchagin
4
5 Permission is hereby granted, free of charge, to any person obtaining a copy
6 of this software and associated documentation files (the "Software"), to deal
7 in the Software without restriction, including without limitation the rights
8 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 copies of the Software, and to permit persons to whom the Software is
10 furnished to do so, subject to the following conditions:
11
12 The above copyright notice and this permission notice shall be included in
13 all copies or substantial portions of the Software.
14
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21*/
22
23#include <QCoreApplication>
24#include <QTcpServer>
25#include <QTcpSocket>
26#include <QTimer>
27#include <cstdio>
28
29// QtCrypto has the declarations for all of QCA
30#include <QtCrypto>
31
32#ifdef QT_STATICPLUGIN
33#include "import_plugins.h"
34#endif
35
36static QString prompt(const QString &s)
37{
38 printf(format: "* %s ", qPrintable(s));
39 fflush(stdout);
40 char line[256];
41 fgets(s: line, n: 255, stdin);
42 QString result = QString::fromLatin1(ba: line);
43 if (result[result.length() - 1] == QLatin1Char('\n'))
44 result.truncate(pos: result.length() - 1);
45 return result;
46}
47
48static QString socketErrorToString(QAbstractSocket::SocketError x)
49{
50 QString s;
51 switch (x) {
52 case QAbstractSocket::ConnectionRefusedError:
53 s = QStringLiteral("connection refused or timed out");
54 break;
55 case QAbstractSocket::RemoteHostClosedError:
56 s = QStringLiteral("remote host closed the connection");
57 break;
58 case QAbstractSocket::HostNotFoundError:
59 s = QStringLiteral("host not found");
60 break;
61 case QAbstractSocket::SocketAccessError:
62 s = QStringLiteral("access error");
63 break;
64 case QAbstractSocket::SocketResourceError:
65 s = QStringLiteral("too many sockets");
66 break;
67 case QAbstractSocket::SocketTimeoutError:
68 s = QStringLiteral("operation timed out");
69 break;
70 case QAbstractSocket::DatagramTooLargeError:
71 s = QStringLiteral("datagram was larger than system limit");
72 break;
73 case QAbstractSocket::NetworkError:
74 s = QStringLiteral("network error");
75 break;
76 case QAbstractSocket::AddressInUseError:
77 s = QStringLiteral("address is already in use");
78 break;
79 case QAbstractSocket::SocketAddressNotAvailableError:
80 s = QStringLiteral("address does not belong to the host");
81 break;
82 case QAbstractSocket::UnsupportedSocketOperationError:
83 s = QStringLiteral("operation is not supported by the local operating system");
84 break;
85 default:
86 s = QStringLiteral("unknown socket error");
87 break;
88 }
89 return s;
90}
91
92static QString saslAuthConditionToString(QCA::SASL::AuthCondition x)
93{
94 QString s;
95 switch (x) {
96 case QCA::SASL::NoMechanism:
97 s = QStringLiteral("no appropriate mechanism could be negotiated");
98 break;
99 case QCA::SASL::BadProtocol:
100 s = QStringLiteral("bad SASL protocol");
101 break;
102 case QCA::SASL::BadServer:
103 s = QStringLiteral("server failed mutual authentication");
104 break;
105 // AuthFail or unknown (including those defined for server only)
106 default:
107 s = QStringLiteral("generic authentication failure");
108 break;
109 };
110 return s;
111}
112
113class ClientTest : public QObject
114{
115 Q_OBJECT
116
117private:
118 QString host, proto, authzid, realm, user, pass;
119 int port;
120 bool no_authzid, no_realm;
121 int mode; // 0 = receive mechanism list, 1 = sasl negotiation, 2 = app
122 QTcpSocket *sock;
123 QCA::SASL *sasl;
124 QByteArray inbuf;
125 bool sock_done;
126 int waitCycles;
127
128public:
129 ClientTest(const QString &_host,
130 int _port,
131 const QString &_proto,
132 const QString &_authzid,
133 const QString &_realm,
134 const QString &_user,
135 const QString &_pass,
136 bool _no_authzid,
137 bool _no_realm)
138 : host(_host)
139 , proto(_proto)
140 , authzid(_authzid)
141 , realm(_realm)
142 , user(_user)
143 , pass(_pass)
144 , port(_port)
145 , no_authzid(_no_authzid)
146 , no_realm(_no_realm)
147 , sock_done(false)
148 , waitCycles(0)
149 {
150 sock = new QTcpSocket(this);
151 connect(sender: sock, signal: &QTcpSocket::connected, context: this, slot: &ClientTest::sock_connected);
152 connect(sender: sock, signal: &QTcpSocket::readyRead, context: this, slot: &ClientTest::sock_readyRead);
153#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
154 connect(sender: sock, signal: &QTcpSocket::errorOccurred, context: this, slot: &ClientTest::sock_error);
155#else
156 connect(sock, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this, &ClientTest::sock_error);
157#endif
158
159 sasl = new QCA::SASL(this);
160 connect(sender: sasl, signal: &QCA::SASL::clientStarted, context: this, slot: &ClientTest::sasl_clientFirstStep);
161 connect(sender: sasl, signal: &QCA::SASL::nextStep, context: this, slot: &ClientTest::sasl_nextStep);
162 connect(sender: sasl, signal: &QCA::SASL::needParams, context: this, slot: &ClientTest::sasl_needParams);
163 connect(sender: sasl, signal: &QCA::SASL::authenticated, context: this, slot: &ClientTest::sasl_authenticated);
164 connect(sender: sasl, signal: &QCA::SASL::readyRead, context: this, slot: &ClientTest::sasl_readyRead);
165 connect(sender: sasl, signal: &QCA::SASL::readyReadOutgoing, context: this, slot: &ClientTest::sasl_readyReadOutgoing);
166 connect(sender: sasl, signal: &QCA::SASL::error, context: this, slot: &ClientTest::sasl_error);
167 }
168
169public Q_SLOTS:
170 void start()
171 {
172 mode = 0; // mech list mode
173
174 int flags = 0;
175 flags |= QCA::SASL::AllowPlain;
176 flags |= QCA::SASL::AllowAnonymous;
177 sasl->setConstraints(f: (QCA::SASL::AuthFlags)flags, minSSF: 0, maxSSF: 256);
178
179 if (!user.isEmpty())
180 sasl->setUsername(user);
181 if (!authzid.isEmpty())
182 sasl->setAuthzid(authzid);
183 if (!pass.isEmpty())
184 sasl->setPassword(pass.toUtf8());
185 if (!realm.isEmpty())
186 sasl->setRealm(realm);
187
188 printf(format: "Connecting to %s:%d, for protocol %s\n", qPrintable(host), port, qPrintable(proto));
189 sock->connectToHost(hostName: host, port);
190 }
191
192Q_SIGNALS:
193 void quit();
194
195private Q_SLOTS:
196 void sock_connected()
197 {
198 printf(format: "Connected to server. Awaiting mechanism list...\n");
199 }
200
201 void sock_error(QAbstractSocket::SocketError x)
202 {
203 if (x == QAbstractSocket::RemoteHostClosedError) {
204 if (mode == 2) // app mode, where disconnect means completion
205 {
206 sock_done = true;
207 tryFinished();
208 return;
209 } else // any other mode, where disconnect is an error
210 {
211 printf(format: "Error: server closed connection unexpectedly.\n");
212 emit quit();
213 return;
214 }
215 }
216
217 printf(format: "Error: socket: %s\n", qPrintable(socketErrorToString(x)));
218 emit quit();
219 }
220
221 void sock_readyRead()
222 {
223 if (mode == 2) // app mode
224 {
225 QByteArray a = sock->readAll();
226 printf(format: "Read %d bytes\n", int(a.size()));
227
228 // there is a possible flaw in the qca 2.0 api, in
229 // that if sasl data is received from the peer
230 // followed by a disconnect from the peer, there is
231 // no clear approach to salvaging the bytes. tls is
232 // not affected because tls has the concept of
233 // closing a session. with sasl, there is no
234 // closing, and since the qca api is asynchronous,
235 // we could potentially wait forever for decoded
236 // data, if the last write was a partial packet.
237 //
238 // for now, we can perform a simple workaround of
239 // waiting at least three event loop cycles for
240 // decoded data before giving up and assuming the
241 // last write was partial. the fact is, all current
242 // qca sasl providers respond within this time
243 // frame, so this fix should work fine for now. in
244 // qca 2.1, we should revise the api to handle this
245 // situation better.
246 //
247 // further note: i guess this only affects application
248 // protocols that have no close message of their
249 // own, and rely on the tcp-level close. examples
250 // are http, and of course this qcatest protocol.
251 if (waitCycles == 0) {
252 waitCycles = 3;
253 QMetaObject::invokeMethod(obj: this, member: "waitWriteIncoming", c: Qt::QueuedConnection);
254 }
255
256 sasl->writeIncoming(a);
257 } else // mech list or sasl negotiation mode
258 {
259 if (sock->canReadLine()) {
260 QString line = QString::fromLatin1(ba: sock->readLine());
261 line.truncate(pos: line.length() - 1); // chop the newline
262 handleLine(line);
263 }
264 }
265 }
266
267 void sasl_clientFirstStep(bool clientInit, const QByteArray &clientInitData)
268 {
269 printf(format: "Choosing mech: %s\n", qPrintable(sasl->mechanism()));
270 QString line = sasl->mechanism();
271 if (clientInit) {
272 line += QLatin1Char(' ');
273 line += arrayToString(ba: clientInitData);
274 }
275 sendLine(line);
276 }
277
278 void sasl_nextStep(const QByteArray &stepData)
279 {
280 QString line = QStringLiteral("C");
281 if (!stepData.isEmpty()) {
282 line += QLatin1Char(',');
283 line += arrayToString(ba: stepData);
284 }
285 sendLine(line);
286 }
287
288 void sasl_needParams(const QCA::SASL::Params &params)
289 {
290 if (params.needUsername()) {
291 user = prompt(QStringLiteral("Username:"));
292 sasl->setUsername(user);
293 }
294
295 if (params.canSendAuthzid() && !no_authzid) {
296 authzid = prompt(QStringLiteral("Authorize As (enter to skip):"));
297 if (!authzid.isEmpty())
298 sasl->setAuthzid(authzid);
299 }
300
301 if (params.needPassword()) {
302 QCA::ConsolePrompt prompt;
303 prompt.getHidden(QStringLiteral("* Password"));
304 prompt.waitForFinished();
305 QCA::SecureArray pass = prompt.result();
306 sasl->setPassword(pass);
307 }
308
309 if (params.canSendRealm() && !no_realm) {
310 QStringList realms = sasl->realmList();
311 printf(format: "Available realms:\n");
312 if (realms.isEmpty())
313 printf(format: " (none specified)\n");
314 foreach (const QString &s, realms)
315 printf(format: " %s\n", qPrintable(s));
316 realm = prompt(QStringLiteral("Realm (enter to skip):"));
317 if (!realm.isEmpty())
318 sasl->setRealm(realm);
319 }
320
321 sasl->continueAfterParams();
322 }
323
324 void sasl_authenticated()
325 {
326 printf(format: "SASL success!\n");
327 printf(format: "SSF: %d\n", sasl->ssf());
328 }
329
330 void sasl_readyRead()
331 {
332 QByteArray a = sasl->read();
333 inbuf += a;
334 processInbuf();
335 }
336
337 void sasl_readyReadOutgoing()
338 {
339 QByteArray a = sasl->readOutgoing();
340 sock->write(data: a);
341 }
342
343 void sasl_error()
344 {
345 int e = sasl->errorCode();
346 if (e == QCA::SASL::ErrorInit)
347 printf(format: "Error: sasl: initialization failed.\n");
348 else if (e == QCA::SASL::ErrorHandshake)
349 printf(format: "Error: sasl: %s.\n", qPrintable(saslAuthConditionToString(sasl->authCondition())));
350 else if (e == QCA::SASL::ErrorCrypt)
351 printf(format: "Error: sasl: broken security layer.\n");
352 else
353 printf(format: "Error: sasl: unknown error.\n");
354
355 emit quit();
356 }
357
358 void waitWriteIncoming()
359 {
360 --waitCycles;
361 if (waitCycles > 0) {
362 QMetaObject::invokeMethod(obj: this, member: "waitWriteIncoming", c: Qt::QueuedConnection);
363 return;
364 }
365
366 tryFinished();
367 }
368
369private:
370 void tryFinished()
371 {
372 if (sock_done && waitCycles == 0) {
373 printf(format: "Finished, server closed connection.\n");
374
375 // if we give up on waiting for a response to
376 // writeIncoming, then it might come late. in
377 // theory this shouldn't happen if we wait enough
378 // cycles, but if one were to arrive then it could
379 // occur between the request to quit the app and
380 // the actual quit of the app. to assist with
381 // debugging, then, we'll explicitly stop listening
382 // for signals here. otherwise the response may
383 // still be received and displayed, giving a false
384 // sense of correctness.
385 sasl->disconnect(receiver: this);
386
387 emit quit();
388 }
389 }
390
391 QString arrayToString(const QByteArray &ba)
392 {
393 return QCA::Base64().arrayToString(a: ba);
394 }
395
396 QByteArray stringToArray(const QString &s)
397 {
398 return QCA::Base64().stringToArray(s).toByteArray();
399 }
400
401 void sendLine(const QString &line)
402 {
403 printf(format: "Writing: {%s}\n", qPrintable(line));
404 QString s = line + QLatin1Char('\n');
405 QByteArray a = s.toUtf8();
406 if (mode == 2) // app mode
407 sasl->write(a); // write to sasl
408 else // mech list or sasl negotiation
409 sock->write(data: a); // write to socket
410 }
411
412 void processInbuf()
413 {
414 // collect completed lines from inbuf
415 QStringList list;
416 int at;
417 while ((at = inbuf.indexOf(c: '\n')) != -1) {
418 list += QString::fromUtf8(ba: inbuf.mid(index: 0, len: at));
419 inbuf = inbuf.mid(index: at + 1);
420 }
421
422 // process the lines
423 foreach (const QString &line, list)
424 handleLine(line);
425 }
426
427 void handleLine(const QString &line)
428 {
429 printf(format: "Reading: [%s]\n", qPrintable(line));
430 if (mode == 0) {
431 // first line is the method list
432 const QStringList mechlist = line.split(sep: QLatin1Char(' '));
433 mode = 1; // switch to sasl negotiation mode
434 sasl->startClient(service: proto, host, mechlist);
435 } else if (mode == 1) {
436 QString type, rest;
437 int n = line.indexOf(c: QLatin1Char(','));
438 if (n != -1) {
439 type = line.mid(position: 0, n);
440 rest = line.mid(position: n + 1);
441 } else
442 type = line;
443
444 if (type == QLatin1String("C")) {
445 sasl->putStep(stepData: stringToArray(s: rest));
446 } else if (type == QLatin1String("E")) {
447 if (!rest.isEmpty())
448 printf(format: "Error: server says: %s.\n", qPrintable(rest));
449 else
450 printf(format: "Error: server error, unspecified.\n");
451 emit quit();
452 return;
453 } else if (type == QLatin1String("A")) {
454 printf(format: "Authentication success.\n");
455 mode = 2; // switch to app mode
456
457 // at this point, the server may send us text
458 // lines for us to display and then close.
459
460 sock_readyRead(); // any extra data?
461 return;
462 } else {
463 printf(format: "Error: Bad format from peer, closing.\n");
464 emit quit();
465 return;
466 }
467 }
468 }
469};
470
471void usage()
472{
473 printf(format: "usage: saslclient (options) host(:port) (user) (pass)\n");
474 printf(format: "options: --proto=x, --authzid=x, --realm=x\n");
475}
476
477int main(int argc, char **argv)
478{
479 QCA::Initializer init;
480 QCoreApplication qapp(argc, argv);
481
482 QStringList args = qapp.arguments();
483 args.removeFirst();
484
485 // options
486 QString proto = QStringLiteral("qcatest"); // default protocol
487 QString authzid, realm;
488 bool no_authzid = false;
489 bool no_realm = false;
490 for (int n = 0; n < args.count(); ++n) {
491 if (!args[n].startsWith(s: QLatin1String("--")))
492 continue;
493
494 QString opt = args[n].mid(position: 2);
495 QString var, val;
496 int at = opt.indexOf(c: QLatin1Char('='));
497 if (at != -1) {
498 var = opt.mid(position: 0, n: at);
499 val = opt.mid(position: at + 1);
500 } else
501 var = opt;
502
503 if (var == QLatin1String("proto")) {
504 proto = val;
505 } else if (var == QLatin1String("authzid")) {
506 // specifying empty authzid means force unspecified
507 if (val.isEmpty())
508 no_authzid = true;
509 else
510 authzid = val;
511 } else if (var == QLatin1String("realm")) {
512 // specifying empty realm means force unspecified
513 if (val.isEmpty())
514 no_realm = true;
515 else
516 realm = val;
517 }
518
519 args.removeAt(i: n);
520 --n; // adjust position
521 }
522
523 if (args.count() < 1) {
524 usage();
525 return 0;
526 }
527
528 QString host, user, pass;
529 int port = 8001; // default port
530
531 QString hostinput = args[0];
532 if (args.count() >= 2)
533 user = args[1];
534 if (args.count() >= 3)
535 pass = args[2];
536
537 int at = hostinput.indexOf(c: QLatin1Char(':'));
538 if (at != -1) {
539 host = hostinput.mid(position: 0, n: at);
540#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 2)
541 port = QStringView(hostinput).mid(pos: at + 1).toInt();
542#else
543 port = hostinput.midRef(at + 1).toInt();
544#endif
545 } else
546 host = hostinput;
547
548 if (!QCA::isSupported(features: "sasl")) {
549 printf(format: "Error: SASL support not found.\n");
550 return 1;
551 }
552
553 ClientTest client(host, port, proto, authzid, realm, user, pass, no_authzid, no_realm);
554 QObject::connect(sender: &client, signal: &ClientTest::quit, context: &qapp, slot: &QCoreApplication::quit);
555 QTimer::singleShot(interval: 0, receiver: &client, slot: &ClientTest::start);
556 qapp.exec();
557
558 return 0;
559}
560
561#include "saslclient.moc"
562

source code of qca/examples/saslclient/saslclient.cpp