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 | |
36 | static 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 | |
48 | static 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 | |
92 | static 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 | |
113 | class ClientTest : public QObject |
114 | { |
115 | Q_OBJECT |
116 | |
117 | private: |
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 | |
128 | public: |
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 | |
169 | public 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 | |
192 | Q_SIGNALS: |
193 | void quit(); |
194 | |
195 | private 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 ¶ms) |
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 | |
369 | private: |
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 | |
471 | void 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 | |
477 | int 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 | |