GCC Code Coverage Report


Directory: ./
File: pdcom5/cyrus_sasl/src/SimpleLoginManager.cpp
Date: 2025-06-29 04:10:41
Exec Total Coverage
Lines: 134 230 58.3%
Branches: 79 236 33.5%

Line Branch Exec Source
1 /*****************************************************************************
2 * vim:tw=78
3 *
4 * Copyright (C) 2021 Richard Hacker (lerichi at gmx dot net),
5 * Florian Pose (fp at igh dot de),
6 * Bjarne von Horn (vh at igh dot de).
7 *
8 * This file is part of the PdCom library.
9 *
10 * The PdCom library is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU Lesser General Public License as published by
12 * the Free Software Foundation, either version 3 of the License, or (at your
13 * option) any later version.
14 *
15 * The PdCom library is distributed in the hope that it will be useful, but
16 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
17 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
18 * License for more details.
19 *
20 * You should have received a copy of the GNU Lesser General Public License
21 * along with the PdCom library. If not, see <http://www.gnu.org/licenses/>.
22 *
23 *****************************************************************************/
24
25 #include "SimpleLoginManagerImpl.h"
26
27 #include <algorithm>
28 #include <array>
29 #include <cstring>
30 #include <iostream>
31 #include <new>
32 #include <pdcom5/Exception.h>
33 #include <pdcom5/Process.h>
34 #include <pdcom5/SimpleLoginManager.h>
35 #include <sasl/sasl.h>
36 #include <sasl/saslutil.h>
37 #include <stdexcept>
38 #include <type_traits>
39
40 // #define DEBUG_SASL
41
42 using SLM = PdCom::SimpleLoginManager;
43 using SLMi = PdCom::impl::SimpleLoginManager;
44
45 namespace {
46 31 std::string bb64decode(const char *data)
47 {
48
2/4
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 31 times.
31 if (!data or !*data)
49 return "";
50
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 const auto b64_len = ::strlen(data);
51 31 unsigned int binary_len = (b64_len + 3) / 4 * 3 + 3;
52
1/2
✓ Branch 4 taken 31 times.
✗ Branch 5 not taken.
62 std::string ans(binary_len, '0');
53
3/6
✓ Branch 1 taken 31 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 31 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 31 times.
62 if (sasl_decode64(data, b64_len, &ans[0], binary_len, &binary_len)
54 31 != SASL_OK)
55 throw PdCom::Exception("sasl_decode64 failed");
56
1/2
✓ Branch 1 taken 31 times.
✗ Branch 2 not taken.
31 ans.resize(binary_len);
57 31 return ans;
58 }
59 59 std::string bb64encode(const char *data, size_t len)
60 {
61 59 unsigned int b64_len = (len + 2) / 3 * 4 + 3;
62
1/2
✓ Branch 3 taken 59 times.
✗ Branch 4 not taken.
59 std::string ans(b64_len, '0');
63
3/6
✓ Branch 1 taken 59 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 59 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 59 times.
59 if (sasl_encode64(data, len, &ans[0], b64_len, &b64_len) != SASL_OK)
64 throw PdCom::Exception("sasl_encode64 failed");
65
1/2
✓ Branch 1 taken 59 times.
✗ Branch 2 not taken.
59 ans.resize(b64_len);
66 59 return ans;
67 }
68 } // namespace
69
70 29 SLM::SimpleLoginManager(
71 const char *remote_host,
72 29 sasl_callback *additional_callbacks) :
73
2/4
✓ Branch 5 taken 29 times.
✗ Branch 6 not taken.
✓ Branch 9 taken 29 times.
✗ Branch 10 not taken.
29 impl_(new SLMi(this, remote_host, additional_callbacks))
74 29 {}
75
76 SLM::SimpleLoginManager(SLM &&o) noexcept :
77 Sasl(std::move(o)), impl_(std::move(o.impl_))
78 {
79 if (impl_)
80 impl_->This_ = this;
81 }
82
83 SLM::~SimpleLoginManager() = default;
84
85 SLM &SLM::operator=(SLM &&o) noexcept
86 {
87 if (&o == this)
88 return *this;
89 static_cast<Sasl &>(*this) = static_cast<Sasl &&>(o);
90 std::swap(impl_, o.impl_);
91
92 if (impl_)
93 impl_->This_ = this;
94 if (o.impl_)
95 o.impl_->This_ = &o;
96 return *this;
97 }
98
99 6 bool SLM::login()
100 {
101
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
6 if (!impl_)
102 throw PdCom::InvalidArgument("Used Moved-from instance!");
103 6 impl_->startNewSession();
104 6 return Sasl::loginStep(nullptr, nullptr);
105 }
106
107 void SLM::log(int /*level*/, const char * /*message*/)
108 {}
109
110 36 void SLMi::SaslContextDeleter::operator()(sasl_conn_t *c) const
111 {
112 36 sasl_dispose(&c);
113 36 }
114
115 struct SLMi::CallbackManager
116 {
117 static int
118 getSimple(void *context, int id, const char **result, unsigned *len);
119 static int getSecret(
120 sasl_conn_t * /*conn*/,
121 void *context,
122 int id,
123 sasl_secret_t **psecret);
124 static int getOption(
125 void *context,
126 const char *plugin_name,
127 const char *option,
128 const char **result,
129 unsigned *len);
130 static int getRealm(
131 void *context,
132 int id,
133 const char **availrealms,
134 const char **result);
135 static int log(void *context, int level, const char *message);
136 static std::unique_ptr<sasl_callback[]> make_callbacks(
137 SimpleLoginManager *primary_context,
138 sasl_callback_t *additional_callbacks);
139 };
140 31 int SLMi::CallbackManager::getSimple(
141 void *context,
142 int id,
143 const char **result,
144 unsigned *len)
145 {
146 31 *result = nullptr;
147
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 if (id != SASL_CB_AUTHNAME)
148 return SASL_FAIL;
149
150 31 SLMi &am = *reinterpret_cast<SLMi *>(context);
151 try {
152
4/7
✓ Branch 8 taken 28 times.
✓ Branch 9 taken 3 times.
✓ Branch 12 taken 28 times.
✗ Branch 13 not taken.
✗ Branch 18 not taken.
✓ Branch 19 taken 3 times.
✗ Branch 20 not taken.
31 am.cached_userdata_.emplace_front(am.This_->getAuthname());
153
1/2
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
28 *result = am.cached_userdata_.front().data();
154
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
28 if (len)
155 *len = am.cached_userdata_.front().size();
156 28 return SASL_OK;
157 }
158 6 catch (SLM::Cancel const &) {
159 3 am.state_ = SLMi::State::Canceled;
160 }
161 catch (std::exception const &) {
162 }
163 3 return SASL_FAIL;
164 }
165
166 28 int SLMi::CallbackManager::getSecret(
167 sasl_conn_t * /*conn*/,
168 void *context,
169 int id,
170 sasl_secret_t **psecret)
171 {
172 28 *psecret = nullptr;
173
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
28 if (id != SASL_CB_PASS)
174 return SASL_FAIL;
175
176 28 SLMi &am = *reinterpret_cast<SLMi *>(context);
177 try {
178
1/5
✓ Branch 7 taken 28 times.
✗ Branch 8 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
56 const auto pw = am.This_->getPassword();
179 // allocate buffer of char's and do some placement new
180 // yes that's annoying but you really don't want UB here...
181 static_assert(
182 std::is_trivially_destructible<sasl_secret_t>::value,
183 "dtor of sasl_secret_t must be skippable");
184
3/4
✓ Branch 3 taken 28 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 356 times.
✓ Branch 6 taken 28 times.
28 am.cached_password_.reset(new char[sizeof(long) + pw.size()]());
185 sasl_secret_t *const secret =
186
1/2
✓ Branch 3 taken 28 times.
✗ Branch 4 not taken.
28 new (am.cached_password_.get()) sasl_secret_t();
187 28 secret->len = pw.size();
188
1/2
✓ Branch 3 taken 28 times.
✗ Branch 4 not taken.
28 std::copy(pw.begin(), pw.end(), &secret->data[0]);
189 28 *psecret = secret;
190 28 return SASL_OK;
191 }
192 catch (SLM::Cancel const &) {
193 am.state_ = SLMi::State::Canceled;
194 }
195 catch (std::exception const &) {
196 }
197 return SASL_FAIL;
198 }
199
200 64 int SLMi::CallbackManager::getOption(
201 void *context,
202 const char *plugin_name,
203 const char *option,
204 const char **result,
205 unsigned *len)
206 {
207 64 *result = nullptr;
208 64 SLMi &am = *reinterpret_cast<SLMi *>(context);
209 try {
210
0/2
✗ Branch 4 not taken.
✗ Branch 5 not taken.
64 am.cached_userdata_.emplace_front(
211
2/5
✗ Branch 6 not taken.
✓ Branch 7 taken 64 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 64 times.
✗ Branch 14 not taken.
64 am.This_->getOption(plugin_name, option));
212 if (am.cached_userdata_.front().empty()) {
213 am.cached_userdata_.pop_front();
214 *result = nullptr;
215 if (len)
216 *len = 0;
217 }
218 else {
219 *result = am.cached_userdata_.front().data();
220 if (len)
221 *len = am.cached_userdata_.front().size();
222 }
223 return SASL_OK;
224 }
225 64 catch (SLM::Cancel const &) {
226 // do not set state to canceled as it is meant to be for username/pw not
227 // supplied
228 }
229 catch (std::exception const &) {
230 }
231 64 return SASL_FAIL;
232 }
233
234
235 int SLMi::CallbackManager::getRealm(
236 void *context,
237 int id,
238 const char **availrealms,
239 const char **result)
240 {
241 if (id != SASL_CB_GETREALM)
242 return SASL_FAIL;
243 *result = nullptr;
244 SLMi &am = *reinterpret_cast<SLMi *>(context);
245 try {
246 std::vector<const char *> avail_reams;
247 if (availrealms) {
248 for (; *availrealms; availrealms++) {
249 avail_reams.push_back(*availrealms);
250 }
251 }
252 am.cached_userdata_.emplace_front(am.This_->getRealm(avail_reams));
253 *result = am.cached_userdata_.front().data();
254 return SASL_OK;
255 }
256 catch (SLM::Cancel const &) {
257 am.state_ = SLMi::State::Canceled;
258 }
259 catch (std::exception const &) {
260 }
261 return SASL_FAIL;
262 }
263
264 int SLMi::CallbackManager::log(void *context, int level, const char *message)
265 {
266 const SLMi &am = *reinterpret_cast<const SLMi *>(context);
267 try {
268 if (am.This_)
269 am.This_->log(level, message);
270 }
271 catch (...) {
272 return SASL_FAIL;
273 }
274
275 return SASL_OK;
276 }
277
278 29 std::unique_ptr<sasl_callback[]> SLMi::CallbackManager::make_callbacks(
279 SimpleLoginManager *primary_context,
280 sasl_callback_t *additional_callbacks)
281 {
282 using cb_t = int (*)();
283 #ifndef _MSC_VER
284 #pragma GCC diagnostic push
285 #pragma GCC diagnostic ignored "-Wcast-function-type"
286 #endif
287 29 const std::array<sasl_callback, 5> primary_callbacks {{
288 {SASL_CB_GETOPT, (cb_t) getOption, primary_context},
289 {SASL_CB_AUTHNAME, (cb_t) CallbackManager::getSimple,
290 primary_context},
291 {SASL_CB_PASS, (cb_t) CallbackManager::getSecret, primary_context},
292 {SASL_CB_GETREALM, (cb_t) CallbackManager::getRealm,
293 primary_context},
294 {SASL_CB_LOG, (cb_t) CallbackManager::log, primary_context},
295 29 }};
296 #ifndef _MSC_VER
297 #pragma GCC diagnostic pop
298 #endif
299 29 size_t count = primary_callbacks.size() + 1;
300
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
29 for (auto i = additional_callbacks; i && i->id != SASL_CB_LIST_END; ++i) {
301 ++count;
302 }
303
4/6
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
✓ Branch 4 taken 29 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 174 times.
✓ Branch 7 taken 29 times.
29 std::unique_ptr<sasl_callback[]> ans {new sasl_callback[count]()};
304
1/2
✓ Branch 4 taken 29 times.
✗ Branch 5 not taken.
29 std::copy(primary_callbacks.begin(), primary_callbacks.end(), ans.get());
305 29 size_t occupied_slots = primary_callbacks.size();
306
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
29 for (auto i = additional_callbacks; i && i->id != SASL_CB_LIST_END; ++i) {
307 const auto current_end = ans.get() + occupied_slots;
308 const auto match = std::find_if(
309 ans.get(), current_end,
310 [i](const sasl_callback_t &e) { return e.id == i->id; });
311 if (match == current_end)
312 ++occupied_slots;
313 *match = *i;
314 }
315
1/2
✓ Branch 3 taken 29 times.
✗ Branch 4 not taken.
29 ans[occupied_slots] = {SASL_CB_LIST_END, nullptr, nullptr};
316 29 return ans;
317 }
318
319 29 SLMi::SimpleLoginManager(
320 SLM *This,
321 const char *host,
322 29 sasl_callback *additional_callbacks) :
323 This_(This),
324 host_(host),
325 callbacks_(CallbackManager::make_callbacks(this, additional_callbacks)),
326
3/6
✓ Branch 5 taken 29 times.
✗ Branch 6 not taken.
✓ Branch 11 taken 29 times.
✗ Branch 12 not taken.
✓ Branch 19 taken 29 times.
✗ Branch 20 not taken.
29 cached_userdata_(5)
327 29 {}
328
329 36 void SLMi::startNewSession()
330 {
331 36 state_ = SimpleLoginManager::State::Init;
332 36 sasl_conn_t *sasl_ctx = nullptr;
333
2/4
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 36 times.
72 if (sasl_client_new(
334 36 "pdserv", host_.c_str(), nullptr, nullptr, callbacks_.get(),
335 SASL_SUCCESS_DATA, &sasl_ctx)
336 36 != SASL_OK)
337 throw PdCom::Exception("sasl client new failed");
338 36 sasl_ctx_.reset(sasl_ctx);
339 36 sasl_ctx = nullptr;
340 36 cached_userdata_.clear();
341 36 cached_password_.reset();
342 36 }
343
344 90 void SLM::loginReply(const char *mechlist, const char *serverData, int finished)
345 {
346
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 90 times.
90 if (!impl_) {
347 throw PdCom::InvalidArgument("Used Moved-from instance!");
348 }
349
350
2/2
✓ Branch 4 taken 27 times.
✓ Branch 5 taken 63 times.
90 if (!impl_->sasl_ctx_) {
351 27 impl_->startNewSession();
352 }
353
354
2/2
✓ Branch 0 taken 62 times.
✓ Branch 1 taken 28 times.
90 if (finished == 0) {
355 using State = SLMi::State;
356 62 sasl_interact_t *interacts = nullptr;
357 62 const char *client_out = nullptr, *mech = nullptr;
358 62 unsigned int client_out_len = 0;
359 62 int res = SASL_FAIL;
360
2/2
✓ Branch 3 taken 31 times.
✓ Branch 4 taken 31 times.
62 if (impl_->state_ == State::Init) {
361 31 impl_->state_ = State::MechRecieved;
362 do {
363
1/2
✓ Branch 2 taken 31 times.
✗ Branch 3 not taken.
31 res = sasl_client_start(
364 31 impl_->sasl_ctx_.get(), mechlist, &interacts,
365 &client_out, &client_out_len, &mech);
366 #ifdef DEBUG_SASL
367 std::cerr << __func__ << "() sasl_client_start " << res
368 << " mechlist " << mechlist << std::endl;
369 #endif
370
1/2
✓ Branch 4 taken 31 times.
✗ Branch 5 not taken.
31 impl_->doInteract(interacts);
371
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 } while (res == SASL_INTERACT);
372 }
373
1/2
✓ Branch 3 taken 31 times.
✗ Branch 4 not taken.
31 else if (impl_->state_ == State::Pending) {
374
1/2
✓ Branch 2 taken 31 times.
✗ Branch 3 not taken.
62 const std::string s = bb64decode(serverData);
375 do {
376
1/2
✓ Branch 3 taken 31 times.
✗ Branch 4 not taken.
62 res = sasl_client_step(
377 62 impl_->sasl_ctx_.get(), s.c_str(), s.size(), &interacts,
378 &client_out, &client_out_len);
379 #ifdef DEBUG_SASL
380 std::cerr << __func__ << "() sasl_client_step " << res
381 << std::endl;
382 #endif
383
1/2
✓ Branch 4 taken 31 times.
✗ Branch 5 not taken.
31 impl_->doInteract(interacts);
384
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 } while (res == SASL_INTERACT);
385 }
386 else {
387 return;
388 }
389
390
3/4
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 31 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
62 switch (res) {
391 28 case SASL_OK:
392 28 impl_->state_ = State::Completed;
393 28 break;
394 31 case SASL_CONTINUE:
395 31 impl_->state_ = State::Pending;
396 31 break;
397 case SASL_NOMECH:
398 impl_->state_ = State::Error;
399 completed(LoginResult::NoSaslMechanism);
400 impl_->sasl_ctx_.reset();
401 return;
402 3 default:
403
1/2
✓ Branch 3 taken 3 times.
✗ Branch 4 not taken.
3 if (impl_->state_ == State::Canceled) {
404
1/2
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
3 completed(LoginResult::Canceled);
405
1/2
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 impl_->startNewSession();
406 }
407 else {
408 impl_->state_ = State::Error;
409 completed(LoginResult::Error);
410 std::cerr << sasl_errdetail(impl_->sasl_ctx_.get())
411 << std::endl;
412 impl_->sasl_ctx_.reset();
413 }
414 3 return;
415 }
416
3/4
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 31 times.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
59 if (res == SASL_CONTINUE or res == SASL_OK) {
417
3/4
✓ Branch 2 taken 59 times.
✗ Branch 3 not taken.
✓ Branch 10 taken 59 times.
✓ Branch 11 taken 3 times.
118 const auto s = bb64encode(client_out, client_out_len);
418
1/2
✓ Branch 5 taken 59 times.
✗ Branch 6 not taken.
59 Sasl::loginStep(mech, s.c_str());
419 }
420 }
421
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 4 times.
28 else if (finished > 0) {
422 24 impl_->sasl_ctx_.reset();
423 24 completed(LoginResult::Success);
424 }
425
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 else if (finished < 0) {
426 4 impl_->sasl_ctx_.reset();
427 4 completed(LoginResult::Error);
428 }
429 }
430
431 62 void SLMi::doInteract(sasl_interact_t *interacts)
432 {
433
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 62 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
62 for (; interacts && interacts->id != SASL_CB_LIST_END; ++interacts) {
434 try {
435 cached_userdata_.emplace_front(This_->interact(
436 interacts->id, interacts->challenge, interacts->prompt,
437 interacts->defresult));
438 }
439 catch (SLM::Cancel const &) {
440 interacts->result = nullptr;
441 continue;
442 }
443 interacts->result = cached_userdata_.front().data();
444 interacts->len = cached_userdata_.front().size();
445 }
446 62 }
447
448 static char plugin_path_buffer[2049];
449
450 static int sasl_get_path(void *, const char **path)
451 {
452 *path = plugin_path_buffer;
453 return SASL_OK;
454 }
455
456 #ifndef _MSC_VER
457 #pragma GCC diagnostic push
458 #pragma GCC diagnostic ignored "-Wcast-function-type"
459 #endif
460 static const sasl_callback_t static_callbacks[] = {
461 {SASL_CB_GETPATH, (int (*)(void))(sasl_get_path), nullptr},
462 {SASL_CB_LIST_END, nullptr, nullptr},
463 };
464 #ifndef _MSC_VER
465 #pragma GCC diagnostic pop
466 #endif
467
468 8 void SLM::InitLibrary(const char *plugin_path)
469 {
470 8 const sasl_callback_t *callbacks = nullptr;
471
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
8 if (plugin_path && *plugin_path) {
472 strncpy(plugin_path_buffer, plugin_path,
473 sizeof(plugin_path_buffer) - 1);
474 plugin_path_buffer[sizeof(plugin_path_buffer) - 1] = 0;
475
476 callbacks = static_callbacks;
477 }
478
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
8 if (sasl_client_init(callbacks) != SASL_OK)
479 throw PdCom::Exception("sasl_client_init() failed");
480 8 }
481
482 5 void SLM::FinalizeLibrary()
483 {
484 5 sasl_client_done();
485 17 }
486