Import Upstream version 0.69.0
[hcoop/debian/courier-authlib.git] / authmysqllib.cpp
CommitLineData
0e333c05
CE
1/*
2** Copyright 2000-2016 Double Precision, Inc. See COPYING for
3** distribution information.
4*/
5
6#include <stdio.h>
7#include <stdlib.h>
8#include <string.h>
9#include <unistd.h>
10#include <ctype.h>
11#include <sys/types.h>
12#include <sys/stat.h>
13#include <mysql.h>
14#include <time.h>
15
16#include "authmysql.h"
17
18extern "C" {
19#include "authmysqlrc.h"
20#include "auth.h"
21#include "courierauthdebug.h"
22};
23
24#include <map>
25#include <sstream>
26#include <algorithm>
27
28#define err courier_auth_err
29
30#include "authconfigfile.h"
31
32class authmysql_connection {
33
34public:
35 MYSQL *mysql;
36 time_t last_time;
37
38 class authmysqlrc_vars {
39 public:
40 std::string server, server_socket, userid, password, database,
41 character_set,
42 sslkey, sslcert, sslcacert,
43 sslcapath, sslcipher,
44
45 defdomain, user_table,
46 uid_field,
47 gid_field,
48 name_field,
49 crypt_field, clear_field,
50 login_field,
51 home_field,
52 maildir_field,
53 defaultdelivery_field,
54 quota_field,
55 options_field,
56 where_clause,
57 select_clause,
58 enumerate_clause,
59 chpass_clause;
60
61 unsigned int server_port;
62 unsigned int server_opt;
63
64 authmysqlrc_vars()
65 : server_port(0), server_opt(0) {}
66 };
67
68 class authmysqlrc_file : public courier::auth::config_file,
69 public authmysqlrc_vars {
70
71 authmysql_connection &conn;
72
73 public:
74
75 authmysqlrc_file &operator=(const authmysqlrc_file &o)
76 {
77 courier::auth::config_file::operator=(o);
78 authmysqlrc_vars::operator=(o);
79 return *this;
80 }
81
82 authmysqlrc_file(authmysql_connection &connArg)
83 : courier::auth::config_file(AUTHMYSQLRC),
84 conn(connArg)
85 {
86 }
87
88 bool do_load()
89 {
90 server=config("MYSQL_SERVER");
91 userid=config("MYSQL_USERNAME");
92 password=config("MYSQL_PASSWORD");
93 database=config("MYSQL_DATABASE");
94 character_set=config("MYSQL_CHARACTER_SET");
95
96 sslkey=config("MYSQL_SSL_KEY");
97 sslcert=config("MYSQL_SSL_CERT");
98 sslcacert=config("MYSQL_SSL_CACERT");
99 sslcapath=config("MYSQL_SSL_CAPATH");
100 sslcipher=config("MYSQL_SSL_CIPHER");
101
102 if ((std::istringstream(config("MYSQL_PORT"))
103 >> server_port).fail())
104 {
105 err("authmysql: cannot parse the MYSQL_PORT "
106 "setting");
107 return false;
108 }
109
110 if ((std::istringstream(config("MYSQL_OPT"))
111 >> server_opt).fail())
112 {
113 err("authmysql: cannot parse the MYSQL_OPT "
114 "setting");
115 return false;
116 }
117 server_socket=config("MYSQL_SOCKET");
118
119 if (!server.size() && !server_socket.size())
120 {
121 err("authmysql: MYSQL_SERVER nor MYSQL_SOCKET set in"
122 AUTHMYSQLRC ".");
123 return false;
124 }
125
126 if (!userid.size())
127 {
128 err("authmysql: MYSQL_USERNAME not set in "
129 AUTHMYSQLRC ".");
130 return false;
131 }
132
133 if (!database.size())
134 {
135 err("authmysql: MYSQL_DATABASE not set in "
136 AUTHMYSQLRC ".");
137 return false;
138 }
139
140 defdomain=config("DEFAULT_DOMAIN");
141 user_table=config("MYSQL_USER_TABLE");
142
143 if (!user_table.size())
144 {
145 err("authmysql: MYSQL_USER_TABLE not set in "
146 AUTHMYSQLRC ".");
147 return false;
148 }
149
150 uid_field=config("MYSQL_UID_FIELD", "uid");
151 gid_field=config("MYSQL_GID_FIELD", "gid");
152 name_field=config("MYSQL_NAME_FIELD", "''");
153 login_field=config("MYSQL_LOGIN_FIELD", "id");
154 home_field=config("MYSQL_HOME_FIELD", "home");
155 maildir_field=config("MYSQL_MAILDIR_FIELD",
156 "''");
157 defaultdelivery_field=
158 config("MYSQL_DEFAULTDELIVERY_FIELD",
159 "''");
160 quota_field=config("MYSQL_QUOTA_FIELD", "''");
161
162 options_field=config("MYSQL_AUXOPTIONS_FIELD",
163 "''");
164 where_clause=config("MYSQL_WHERE_CLAUSE",
165 "1=1");
166 select_clause=config("MYSQL_SELECT_CLAUSE");
167 enumerate_clause=config("MYSQL_ENUMERATE_CLAUSE");
168
169 chpass_clause=config("MYSQL_CHPASS_CLAUSE");
170 crypt_field=config("MYSQL_CRYPT_PWFIELD", "''");
171 clear_field=config("MYSQL_CLEAR_PWFIELD", "''");
172
173 if (crypt_field == "''" && clear_field == "''")
174 {
175 err("authmysql: MYSQL_CRYPT_PWFIELD and "
176 "MYSQL_CLEAR_PWFIELD not set in "
177 AUTHMYSQLRC ".");
178 return false;
179 }
180
181 return true;
182 }
183
184 void do_reload()
185 {
186 authmysqlrc_file new_file(conn);
187
188 if (new_file.load(true))
189 {
190 *this=new_file;
191 DPRINTF("authmysql: reloaded %s", filename);
192
193 // Disconnect from the server, new login
194 // parameters, perhaps.
195
196 conn.cleanup();
197 }
198 }
199 };
200
201 authmysqlrc_file config_file;
202
203 authmysql_connection() : mysql(0), last_time(0), config_file(*this)
204 {
205 }
206
207 ~authmysql_connection()
208 {
209 cleanup();
210 }
211
212 void cleanup()
213 {
214 if (mysql)
215 {
216 mysql_close(mysql);
217 delete mysql;
218 mysql=0;
219 }
220 }
221
222 static authmysql_connection *singleton;
223
224 bool try_connection()
225 {
226 bool rc=check_connection();
227
228 if (!rc)
229 cleanup();
230 return rc;
231 }
232
233 bool check_connection();
234
235 class result {
236
237 MYSQL_RES *res;
238 MYSQL_ROW row;
239
240 size_t num_fields_n;
241 unsigned long *lengths;
242 public:
243 result(const authmysql_connection &conn, bool use_result=false)
244 : res(use_result
245 ? mysql_use_result(conn.mysql)
246 : mysql_store_result(conn.mysql)), row(NULL),
247 num_fields_n(0),
248 lengths(NULL)
249 {
250 }
251
252 ~result()
253 {
254 if (res)
255 mysql_free_result(res);
256 }
257
258 operator bool() const
259 {
260 return res != NULL;
261 }
262
263 size_t num_rows() const
264 {
265 return res ? mysql_num_rows(res):0;
266 }
267
268 size_t num_fields() const { return num_fields_n; }
269
270 bool fetch()
271 {
272 if (res && (row=mysql_fetch_row(res)) != NULL)
273 {
274 num_fields_n=mysql_num_fields(res);
275 lengths=mysql_fetch_lengths(res);
276 return true;
277 }
278 num_fields_n=0;
279 lengths=NULL;
280 return false;
281 }
282
283 std::string operator[](size_t column) const
284 {
285 if (column < num_fields())
286 {
287 const char *p=reinterpret_cast<const char *>
288 (row[column]);
289
290 return std::string(p, p + lengths[column]);
291 }
292 return std::string();
293 }
294 };
295
296 bool query(const std::string &sql)
297 {
298 if (mysql_query(mysql, sql.c_str()) == 0)
299 return true;
300
301 DPRINTF("mysql_query failed: %s", mysql_error(mysql));
302 cleanup();
303
304 if (!try_connection())
305 return false;
306
307 if (mysql_query (mysql, sql.c_str()))
308 {
309 DPRINTF("mysql_query failed second time, giving up: %s", mysql_error(mysql));
310
311 cleanup();
312 return false;
313 }
314 return true;
315 }
316
317 std::string escape(const std::string &s)
318 {
319 std::string buffer;
320 size_t n=s.size()*2+1;
321
322 buffer.resize(n);
323
324 mysql_real_escape_string(mysql, &buffer[0],
325 s.c_str(), s.size());
326 buffer.resize(strlen(&buffer[0]));
327 return buffer;
328 }
329
330 std::string get_default_select(const char *username,
331 const char *service);
332
333 bool getuserinfo(const char *username,
334 const char *service,
335 authmysqluserinfo &uiret);
336
337 bool setpass(const char *user, const char *pass,
338 const char *oldpass);
339
340 void enumerate( void(*cb_func)(const char *name,
341 uid_t uid,
342 gid_t gid,
343 const char *homedir,
344 const char *maildir,
345 const char *options,
346 void *void_arg),
347 void *void_arg);
348
349 static bool connect()
350 {
351 if (!singleton)
352 singleton=new authmysql_connection;
353
354 if (!singleton->config_file.load())
355 return false;
356
357 return singleton->try_connection();
358 }
359};
360
361authmysql_connection *authmysql_connection::singleton=0;
362
363bool authmysql_connection::check_connection()
364{
365 bool use_ssl=false;
366
367 /*
368 ** Periodically detect dead connections.
369 */
370 if (mysql)
371 {
372 time_t t_check;
373
374 time(&t_check);
375
376 if (t_check < last_time)
377 last_time=t_check; /* System clock changed */
378
379 if (t_check < last_time + 60)
380 return true;
381
382 last_time=t_check;
383
384 if (mysql_ping(mysql) == 0) return true;
385
386 DPRINTF("authmysqllib: mysql_ping failed, connection lost");
387 cleanup();
388 }
389
390 if (config_file.sslcacert.size() || config_file.sslcapath.size())
391 {
392 if (config_file.sslcert.size())
393 DPRINTF("authmysqllib: certificate file set to %s",
394 config_file.sslcert.c_str());
395
396 if (config_file.sslcipher.size())
397 DPRINTF("authmysqllib: ciphers set to %s",
398 config_file.sslcipher.c_str());
399
400 if (config_file.sslcacert.size())
401 DPRINTF("authmysqllib: certificate authority set to %s",
402 config_file.sslcacert.c_str());
403 if (config_file.sslcapath.size())
404 DPRINTF("authmysqllib: certificate authority set to %s",
405 config_file.sslcapath.c_str());
406 use_ssl=true;
407 }
408
409 MYSQL *conn=new MYSQL;
410
411 mysql_init(conn);
412 if (use_ssl)
413 {
414 const char *key=config_file.sslkey.c_str();
415 const char *cert=config_file.sslcert.c_str();
416 const char *cacert=config_file.sslcacert.c_str();
417 const char *capath=config_file.sslcapath.c_str();
418 const char *cipher=config_file.sslcipher.c_str();
419
420 if (!*key) key=0;
421 if (!*cert) cert=0;
422 if (!*cacert) cacert=0;
423 if (!*capath) capath=0;
424 if (!*cipher) cipher=0;
425
426 mysql_ssl_set(conn, key, cert, cacert,
427 capath, cipher);
428 }
429
430 mysql=mysql_real_connect(conn, config_file.server.c_str(),
431 config_file.userid.c_str(),
432 config_file.password.c_str(),
433 NULL,
434 config_file.server_port,
435 (config_file.server_socket.size() ?
436 config_file.server_socket.c_str():0),
437 config_file.server_opt);
438
439 if (!mysql)
440 {
441 err("failed to connect to mysql server (server=%s, userid=%s): %s",
442 config_file.server.size() ? config_file.server.c_str() : "<null>",
443 config_file.userid.size() ? config_file.userid.c_str() : "<null>",
444 mysql_error(conn));
445
446 delete conn;
447 return false;
448 }
449
450 if (mysql_select_db(mysql, config_file.database.c_str()))
451 {
452 err("authmysql: mysql_select_db(%s) error: %s",
453 config_file.database.c_str(), mysql_error(mysql));
454 return false;
455 }
456
457 DPRINTF("authmysqllib: connected. Versions: "
458 "header %lu, "
459 "client %lu, "
460 "server %lu",
461 (long)MYSQL_VERSION_ID,
462 mysql_get_client_version(),
463 mysql_get_server_version(mysql));
464
465 if (config_file.character_set.size())
466 {
467 mysql_set_character_set(mysql,
468 config_file.character_set.c_str());
469
470 std::string real_character_set=mysql_character_set_name(mysql);
471
472 if (config_file.character_set != real_character_set)
473 {
474 err("Cannot set character set to \"%s\","
475 " using \"%s\"\n",
476 config_file.character_set.c_str(),
477 real_character_set.c_str());
478 }
479 else
480 {
481 DPRINTF("Using character set: %s",
482 config_file.character_set.c_str());
483 }
484 }
485
486 return true;
487}
488
489std::string authmysql_connection::get_default_select(const char *username,
490 const char *service)
491{
492 std::string q;
493
494
495 std::string maildir_field=
496 service && strcmp(service, "courier")==0
497 ? config_file.defaultdelivery_field
498 : config_file.maildir_field;
499
500 bool has_domain=strchr(username, '@') != NULL;
501
502 std::ostringstream o;
503
504 o << "SELECT "
505 << config_file.login_field << ", "
506 << config_file.crypt_field << ", "
507 << config_file.clear_field << ", "
508 << config_file.uid_field << ", "
509 << config_file.gid_field << ", "
510 << config_file.home_field << ", "
511 << maildir_field << ", "
512 << config_file.quota_field << ", "
513 << config_file.name_field << ", "
514 << config_file.options_field
515 << " FROM "
516 << config_file.user_table
517 << " WHERE "
518 << config_file.login_field
519 << " = '"
520 << escape(username);
521
522 if (!has_domain && config_file.defdomain.size())
523 {
524 o << "@" << config_file.defdomain;
525 }
526 o << "' AND (" << config_file.where_clause << ")";
527
528 q=o.str();
529 return q;
530}
531
532bool authmysql_connection::getuserinfo(const char *username,
533 const char *service,
534 authmysqluserinfo &uiret)
535{
536 std::string querybuf;
537
538 if (config_file.select_clause.empty())
539 {
540 querybuf=get_default_select(username, service);
541 }
542 else
543 {
544 std::map<std::string, std::string> parameters;
545
546 parameters["service"]=service;
547
548 querybuf=config_file
549 .parse_custom_query(config_file.select_clause,
550 escape(username),
551 config_file.defdomain,
552 parameters);
553 }
554
555 DPRINTF("SQL query: %s", querybuf.c_str());
556
557 if (!query(querybuf))
558 return false;
559
560 result res(*this);
561
562 if (res.fetch())
563 {
564 if (res.num_fields() < 6)
565 {
566 DPRINTF("incomplete row, only %d fields returned",
567 res.num_fields());
568 return false;
569 }
570
571 uiret.username=res[0];
572 uiret.cryptpw=res[1];
573 uiret.clearpw=res[2];
574
575 std::string uid_s=res[3];
576 std::string gid_s=res[4];
577
578 std::istringstream(uid_s) >> uiret.uid;
579 std::istringstream(gid_s) >> uiret.gid;
580
581 if (uiret.uid == 0)
582 {
583 DPRINTF("invalid value for uid: '%s'",
584 uid_s.size() ? uid_s.c_str() : "<null>");
585 return false;
586 }
587
588 if (uiret.gid == 0)
589 {
590 DPRINTF("invalid value for gid: '%s'",
591 gid_s.size() ? gid_s.c_str() : "<null>");
592 return false;
593 }
594 uiret.home=res[5];
595
596 if (uiret.home.size() == 0)
597 {
598 DPRINTF("required value for 'home' (column 6) is missing");
599 return false;
600 }
601
602 uiret.maildir=res[6];
603 uiret.quota=res[7];
604 uiret.fullname=res[8];
605 uiret.options=res[9];
606 }
607 else
608 {
609 DPRINTF("zero rows returned");
610 }
611 return true;
612}
613
614bool authmysql_connection::setpass(const char *user, const char *pass,
615 const char *oldpass)
616{
617 std::string newpass_crypt;
618
619 {
620 char *p;
621
622 if (!(p=authcryptpasswd(pass, oldpass)))
623 return false;
624
625 newpass_crypt=p;
626 free(p);
627 }
628
629 std::string clear_escaped=escape(pass);
630
631 std::string crypt_escaped=escape(newpass_crypt);
632
633 std::string sql_buf;
634
635 if (config_file.chpass_clause.size() == 0)
636 {
637 std::string username_escaped=escape(user);
638
639 bool has_domain=strchr(user, '@') != NULL;
640
641 std::ostringstream o;
642
643 o << "UPDATE " << config_file.user_table << " SET ";
644
645 if (config_file.clear_field != "''")
646 o << config_file.clear_field << "='"
647 << clear_escaped << "'";
648
649 if (config_file.crypt_field != "''")
650 {
651 if (config_file.clear_field != "''") o << ", ";
652 o << config_file.crypt_field << "='" << crypt_escaped << "'";
653 }
654
655 o << " WHERE " << config_file.login_field << "='"
656 << username_escaped;
657
658 if (!has_domain && config_file.defdomain.size())
659 o << "@" << config_file.defdomain;
660 o << "'";
661
662 sql_buf=o.str();
663 }
664 else
665 {
666 std::map<std::string, std::string> parameters;
667
668 parameters["newpass"]=clear_escaped;
669 parameters["newpass_crypt"]=crypt_escaped;
670
671 sql_buf=config_file
672 .parse_custom_query(config_file.chpass_clause,
673 user,
674 config_file.defdomain,
675 parameters);
676 }
677
678 if (courier_authdebug_login_level >= 2)
679 {
680 DPRINTF("setpass SQL: %s", sql_buf.c_str());
681 }
682
683 if (!query(sql_buf))
684 return false;
685 return true;
686}
687
688void authmysql_connection::enumerate(void(*cb_func)(const char *name,
689 uid_t uid,
690 gid_t gid,
691 const char *homedir,
692 const char *maildir,
693 const char *options,
694 void *void_arg),
695 void *void_arg)
696{
697 std::string querybuf;
698
699 if (config_file.enumerate_clause.empty())
700 {
701 std::ostringstream o;
702
703 o << "SELECT "
704 << config_file.login_field << ", "
705 << config_file.uid_field << ", "
706 << config_file.gid_field << ", "
707 << config_file.home_field << ", "
708 << config_file.maildir_field << ", "
709 << config_file.options_field << " FROM "
710 << config_file.user_table << " WHERE "
711 << config_file.where_clause;
712
713 querybuf=o.str();
714 }
715 else
716 {
717 std::map<std::string, std::string> parameters;
718
719 parameters["service"]="enumerate";
720 querybuf=config_file
721 .parse_custom_query(config_file.enumerate_clause, "*",
722 config_file.defdomain, parameters);
723 }
724 DPRINTF("authmysql: enumerate query: %s", querybuf.c_str());
725
726 if (!query(querybuf))
727 return;
728
729 result row(*this);
730
731 if (row)
732 {
733 while (row.fetch())
734 {
735 std::string username=row[0];
736
737 if (username.size() == 0)
738 continue;
739
740 uid_t uid=0;
741 gid_t gid=0;
742
743 std::istringstream(row[1]) >> uid;
744 std::istringstream(row[2]) >> gid;
745 std::string homedir=row[3];
746 std::string maildir=row[4];
747 std::string options=row[5];
748
749 (*cb_func)(username.c_str(), uid, gid,
750 homedir.c_str(),
751 maildir.size() ? maildir.c_str():NULL,
752 options.size() ? options.c_str():NULL,
753 void_arg);
754 }
755 }
756 /* NULL row could indicate end of result or an error */
757 if (mysql_errno(mysql))
758 {
759 DPRINTF("mysql error during enumeration: %s",
760 mysql_error(mysql));
761 }
762 else
763 (*cb_func)(NULL, 0, 0, NULL, NULL, NULL, void_arg);
764}
765
766void auth_mysql_cleanup()
767{
768 if (authmysql_connection::singleton)
769 {
770 delete authmysql_connection::singleton;
771 authmysql_connection::singleton=0;
772 }
773}
774
775bool auth_mysql_setpass(const char *user, const char *pass,
776 const char *oldpass)
777{
778 if (!authmysql_connection::connect())
779 return false;
780 return authmysql_connection::singleton->setpass(user, pass, oldpass);
781}
782
783void auth_mysql_enumerate( void(*cb_func)(const char *name,
784 uid_t uid,
785 gid_t gid,
786 const char *homedir,
787 const char *maildir,
788 const char *options,
789 void *void_arg),
790 void *void_arg)
791{
792 if (!authmysql_connection::connect())
793 {
794 (*cb_func)(NULL, 0, 0, NULL, NULL, NULL, void_arg);
795 return;
796 }
797
798 authmysql_connection::singleton->enumerate(cb_func, void_arg);
799}
800
801bool auth_mysql_getuserinfo(const char *username,
802 const char *service,
803 authmysqluserinfo &uiret)
804{
805 if (!authmysql_connection::connect())
806 return false;
807
808 return authmysql_connection::singleton->getuserinfo(username, service,
809 uiret);
810}