1 /*
2   Copyright 2021 Northern.tech AS
3 
4   This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5 
6   This program is free software; you can redistribute it and/or modify it
7   under the terms of the GNU General Public License as published by the
8   Free Software Foundation; version 3.
9 
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14 
15   You should have received a copy of the GNU General Public License
16   along with this program; if not, write to the Free Software
17   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
18 
19   To the extent this program is licensed as part of the Enterprise
20   versions of CFEngine, the applicable Commercial Open Source License
21   (COSL) may apply to this file if you as a licensee so wish it. See
22   included file COSL.txt.
23 */
24 
25 #include <cf_sql.h>
26 
27 #ifdef HAVE_MYSQL_H
28 # include <mysql.h>
29 #elif defined(HAVE_MYSQL_MYSQL_H)
30 # include <mysql/mysql.h>
31 #endif
32 
33 #ifdef HAVE_PGSQL_LIBPQ_FE_H
34 # include <pgsql/libpq-fe.h>
35 #elif defined(HAVE_LIBPQ_FE_H)
36 # include <libpq-fe.h>
37 #endif
38 
39 /* CFEngine connectors for sql databases. Note that there are significant
40    differences in db admin functions in the various implementations. e.g.
41    sybase/mysql "use database, create database" not in postgres.
42 */
43 
44 /*****************************************************************************/
45 
46 #ifdef HAVE_LIBMYSQLCLIENT
47 
48 typedef struct
49 {
50     MYSQL conn;
51     MYSQL_RES *res;
52 } DbMysqlConn;
53 
54 /*****************************************************************************/
55 
CfConnectMysqlDB(const char * host,const char * user,const char * password,const char * database)56 static DbMysqlConn *CfConnectMysqlDB(const char *host, const char *user, const char *password, const char *database)
57 {
58     DbMysqlConn *c;
59 
60     Log(LOG_LEVEL_VERBOSE, "This is a MySQL database");
61 
62     c = xcalloc(1, sizeof(DbMysqlConn));
63 
64     mysql_init(&c->conn);
65 
66     if (!mysql_real_connect(&c->conn, host, user, password, database, 0, NULL, 0))
67     {
68         Log(LOG_LEVEL_ERR, "Failed to connect to existing MySQL database '%s'", mysql_error(&c->conn));
69         free(c);
70         return NULL;
71     }
72 
73     return c;
74 }
75 
76 /*****************************************************************************/
77 
CfCloseMysqlDB(DbMysqlConn * c)78 static void CfCloseMysqlDB(DbMysqlConn *c)
79 {
80     mysql_close(&c->conn);
81     free(c);
82 }
83 
84 /*****************************************************************************/
85 
CfNewQueryMysqlDb(CfdbConn * cfdb,const char * query)86 static void CfNewQueryMysqlDb(CfdbConn *cfdb, const char *query)
87 {
88     DbMysqlConn *mc = cfdb->data;
89 
90     if (mysql_query(&mc->conn, query) != 0)
91     {
92         Log(LOG_LEVEL_INFO, "MySQL query failed '%s'. (mysql_query: %s)", query, mysql_error(&mc->conn));
93     }
94     else
95     {
96         mc->res = mysql_store_result(&mc->conn);
97 
98         if (mc->res)
99         {
100             cfdb->result = true;
101             cfdb->maxcolumns = mysql_num_fields(mc->res);
102             cfdb->maxrows = mysql_num_rows(mc->res);
103         }
104     }
105 }
106 
107 /*****************************************************************************/
108 
CfFetchMysqlRow(CfdbConn * cfdb)109 static void CfFetchMysqlRow(CfdbConn *cfdb)
110 {
111     int i;
112     MYSQL_ROW thisrow;
113     DbMysqlConn *mc = cfdb->data;
114 
115     if (cfdb->maxrows > 0)
116     {
117         thisrow = mysql_fetch_row(mc->res);
118 
119         if (thisrow)
120         {
121             cfdb->rowdata = xmalloc(sizeof(char *) * cfdb->maxcolumns);
122 
123             for (i = 0; i < cfdb->maxcolumns; i++)
124             {
125                 cfdb->rowdata[i] = (char *) thisrow[i];
126             }
127         }
128         else
129         {
130             cfdb->rowdata = NULL;
131         }
132     }
133 }
134 
135 /*****************************************************************************/
136 
CfDeleteMysqlQuery(CfdbConn * cfdb)137 static void CfDeleteMysqlQuery(CfdbConn *cfdb)
138 {
139     DbMysqlConn *mc = cfdb->data;
140 
141     if (mc->res)
142     {
143         mysql_free_result(mc->res);
144         mc->res = NULL;
145     }
146 }
147 
148 #else
149 
CfConnectMysqlDB(ARG_UNUSED const char * host,ARG_UNUSED const char * user,ARG_UNUSED const char * password,ARG_UNUSED const char * database)150 static void *CfConnectMysqlDB(ARG_UNUSED const char *host, ARG_UNUSED const char *user, ARG_UNUSED const char *password, ARG_UNUSED const char *database)
151 {
152     Log(LOG_LEVEL_INFO, "There is no MySQL support compiled into this version");
153     return NULL;
154 }
155 
CfCloseMysqlDB(ARG_UNUSED void * c)156 static void CfCloseMysqlDB(ARG_UNUSED void *c)
157 {
158 }
159 
CfNewQueryMysqlDb(ARG_UNUSED CfdbConn * cfdb,ARG_UNUSED const char * query)160 static void CfNewQueryMysqlDb(ARG_UNUSED CfdbConn *cfdb, ARG_UNUSED const char *query)
161 {
162 }
163 
CfFetchMysqlRow(ARG_UNUSED CfdbConn * cfdb)164 static void CfFetchMysqlRow(ARG_UNUSED CfdbConn *cfdb)
165 {
166 }
167 
CfDeleteMysqlQuery(ARG_UNUSED CfdbConn * cfdb)168 static void CfDeleteMysqlQuery(ARG_UNUSED CfdbConn *cfdb)
169 {
170 }
171 
172 #endif
173 
174 #if defined(HAVE_LIBPQ) &&\
175     (defined(HAVE_PGSQL_LIBPQ_FE_H) || defined(HAVE_LIBPQ_FE_H))
176 
177 typedef struct
178 {
179     PGconn *conn;
180     PGresult *res;
181 } DbPostgresqlConn;
182 
183 /*****************************************************************************/
184 
CfConnectPostgresqlDB(const char * host,const char * user,const char * password,const char * database)185 static DbPostgresqlConn *CfConnectPostgresqlDB(const char *host,
186                                                const char *user, const char *password, const char *database)
187 {
188     DbPostgresqlConn *cfdb;
189     char format[CF_BUFSIZE];
190 
191     Log(LOG_LEVEL_VERBOSE, "This is a PotsgreSQL database");
192 
193     cfdb = xcalloc(1, sizeof(DbPostgresqlConn));
194 
195     if (strcmp(host, "localhost") == 0)
196     {
197         /* Some authentication problem - ?? */
198         if (database)
199         {
200             snprintf(format, CF_BUFSIZE - 1, "dbname=%s user=%s password=%s", database, user, password);
201         }
202         else
203         {
204             snprintf(format, CF_BUFSIZE - 1, "user=%s password=%s", user, password);
205         }
206     }
207     else
208     {
209         if (database)
210         {
211             snprintf(format, CF_BUFSIZE - 1, "dbname=%s host=%s user=%s password=%s", database, host, user, password);
212         }
213         else
214         {
215             snprintf(format, CF_BUFSIZE - 1, "host=%s user=%s password=%s", host, user, password);
216         }
217     }
218 
219     cfdb->conn = PQconnectdb(format);
220 
221     if (PQstatus(cfdb->conn) == CONNECTION_BAD)
222     {
223         Log(LOG_LEVEL_ERR, "Failed to connect to existing PostgreSQL database. (PQconnectdb: %s)", PQerrorMessage(cfdb->conn));
224         free(cfdb);
225         return NULL;
226     }
227 
228     return cfdb;
229 }
230 
231 /*****************************************************************************/
232 
CfClosePostgresqlDb(DbPostgresqlConn * c)233 static void CfClosePostgresqlDb(DbPostgresqlConn *c)
234 {
235     PQfinish(c->conn);
236     free(c);
237 }
238 
239 /*****************************************************************************/
240 
CfNewQueryPostgresqlDb(CfdbConn * cfdb,const char * query)241 static void CfNewQueryPostgresqlDb(CfdbConn *cfdb, const char *query)
242 {
243     DbPostgresqlConn *pc = cfdb->data;
244 
245     pc->res = PQexec(pc->conn, query);
246 
247     if (PQresultStatus(pc->res) != PGRES_COMMAND_OK && PQresultStatus(pc->res) != PGRES_TUPLES_OK)
248     {
249         Log(LOG_LEVEL_INFO, "PostgreSQL query '%s' failed. (PQExec: %s)", query, PQerrorMessage(pc->conn));
250     }
251     else
252     {
253         cfdb->result = true;
254         cfdb->maxcolumns = PQnfields(pc->res);
255         cfdb->maxrows = PQntuples(pc->res);
256     }
257 }
258 
259 /*****************************************************************************/
260 
CfFetchPostgresqlRow(CfdbConn * cfdb)261 static void CfFetchPostgresqlRow(CfdbConn *cfdb)
262 {
263     assert(cfdb != NULL);
264     DbPostgresqlConn *pc = cfdb->data;
265 
266     if (cfdb->row < 0 || (unsigned int) cfdb->row >= cfdb->maxrows)
267     {
268         cfdb->rowdata = NULL;
269         return;
270     }
271 
272     if (cfdb->maxrows > 0)
273     {
274         cfdb->rowdata = xmalloc(sizeof(char *) * cfdb->maxcolumns);
275     }
276 
277     for (unsigned int i = 0; i < cfdb->maxcolumns; i++)
278     {
279         cfdb->rowdata[i] = PQgetvalue(pc->res, cfdb->row, i);
280     }
281 }
282 
283 /*****************************************************************************/
284 
CfDeletePostgresqlQuery(CfdbConn * cfdb)285 static void CfDeletePostgresqlQuery(CfdbConn *cfdb)
286 {
287     DbPostgresqlConn *pc = cfdb->data;
288 
289     PQclear(pc->res);
290 }
291 
292 /*****************************************************************************/
293 
294 #else
295 
CfConnectPostgresqlDB(ARG_UNUSED const char * host,ARG_UNUSED const char * user,ARG_UNUSED const char * password,ARG_UNUSED const char * database)296 static void *CfConnectPostgresqlDB(ARG_UNUSED const char *host, ARG_UNUSED const char *user, ARG_UNUSED const char *password, ARG_UNUSED const char *database)
297 {
298     Log(LOG_LEVEL_INFO, "There is no PostgreSQL support compiled into this version");
299     return NULL;
300 }
301 
CfClosePostgresqlDb(ARG_UNUSED void * c)302 static void CfClosePostgresqlDb(ARG_UNUSED void *c)
303 {
304 }
305 
CfNewQueryPostgresqlDb(ARG_UNUSED CfdbConn * cfdb,ARG_UNUSED const char * query)306 static void CfNewQueryPostgresqlDb(ARG_UNUSED CfdbConn *cfdb, ARG_UNUSED const char *query)
307 {
308 }
309 
CfFetchPostgresqlRow(ARG_UNUSED CfdbConn * cfdb)310 static void CfFetchPostgresqlRow(ARG_UNUSED CfdbConn *cfdb)
311 {
312 }
313 
CfDeletePostgresqlQuery(ARG_UNUSED CfdbConn * cfdb)314 static void CfDeletePostgresqlQuery(ARG_UNUSED CfdbConn *cfdb)
315 {
316 }
317 
318 #endif
319 
320 /*****************************************************************************/
321 
CfConnectDB(CfdbConn * cfdb,DatabaseType dbtype,char * remotehost,char * dbuser,char * passwd,char * db)322 void CfConnectDB(CfdbConn *cfdb, DatabaseType dbtype, char *remotehost, char *dbuser, char *passwd, char *db)
323 {
324 
325     cfdb->connected = false;
326     cfdb->type = dbtype;
327 
328 /* If db == NULL, no database was specified, so we assume it has not been created yet. Need to
329    open a generic database and create */
330 
331     if (db == NULL)
332     {
333         db = "no db specified";
334     }
335 
336     Log(LOG_LEVEL_VERBOSE, "Connect to SQL database '%s', user '%s', host '%s', type %d", db, dbuser, remotehost,
337           dbtype);
338 
339     switch (dbtype)
340     {
341     case DATABASE_TYPE_MYSQL:
342         cfdb->data = CfConnectMysqlDB(remotehost, dbuser, passwd, db);
343         break;
344 
345     case DATABASE_TYPE_POSTGRES:
346         cfdb->data = CfConnectPostgresqlDB(remotehost, dbuser, passwd, db);
347         break;
348 
349     default:
350         Log(LOG_LEVEL_VERBOSE, "There is no SQL database selected");
351         break;
352     }
353 
354     if (cfdb->data)
355         cfdb->connected = true;
356 }
357 
358 /*****************************************************************************/
359 
CfCloseDB(CfdbConn * cfdb)360 void CfCloseDB(CfdbConn *cfdb)
361 {
362     if (!cfdb->connected)
363     {
364         return;
365     }
366 
367     switch (cfdb->type)
368     {
369     case DATABASE_TYPE_MYSQL:
370         CfCloseMysqlDB(cfdb->data);
371         break;
372 
373     case DATABASE_TYPE_POSTGRES:
374         CfClosePostgresqlDb(cfdb->data);
375         break;
376 
377     default:
378         Log(LOG_LEVEL_VERBOSE, "There is no SQL database selected");
379         break;
380     }
381 
382     cfdb->connected = false;
383 }
384 
385 /*****************************************************************************/
386 
CfVoidQueryDB(CfdbConn * cfdb,char * query)387 void CfVoidQueryDB(CfdbConn *cfdb, char *query)
388 {
389     if (!cfdb->connected)
390     {
391         return;
392     }
393 
394 /* If we don't need to retrieve table entries...*/
395     CfNewQueryDB(cfdb, query);
396     CfDeleteQuery(cfdb);
397 }
398 
399 /*****************************************************************************/
400 
CfNewQueryDB(CfdbConn * cfdb,char * query)401 void CfNewQueryDB(CfdbConn *cfdb, char *query)
402 {
403     cfdb->result = false;
404     cfdb->row = 0;
405     cfdb->column = 0;
406     cfdb->rowdata = NULL;
407     cfdb->maxcolumns = 0;
408     cfdb->maxrows = 0;
409 
410     Log(LOG_LEVEL_DEBUG, "Before query '%s' succeeded, maxrows %d, maxcolumns %d", query, cfdb->maxrows, cfdb->maxcolumns);
411 
412     switch (cfdb->type)
413     {
414     case DATABASE_TYPE_MYSQL:
415         CfNewQueryMysqlDb(cfdb, query);
416         break;
417 
418     case DATABASE_TYPE_POSTGRES:
419         CfNewQueryPostgresqlDb(cfdb, query);
420         break;
421 
422     default:
423         Log(LOG_LEVEL_VERBOSE, "There is no SQL database selected");
424         break;
425     }
426 
427     Log(LOG_LEVEL_DEBUG, "Query '%s' succeeded. maxrows %d, maxcolumns %d", query, cfdb->maxrows, cfdb->maxcolumns);
428 }
429 
430 /*****************************************************************************/
431 
CfFetchRow(CfdbConn * cfdb)432 char **CfFetchRow(CfdbConn *cfdb)
433 {
434     switch (cfdb->type)
435     {
436     case DATABASE_TYPE_MYSQL:
437         CfFetchMysqlRow(cfdb);
438         break;
439 
440     case DATABASE_TYPE_POSTGRES:
441         CfFetchPostgresqlRow(cfdb);
442         break;
443 
444     default:
445         Log(LOG_LEVEL_VERBOSE, "There is no SQL database selected");
446         break;
447     }
448 
449     cfdb->row++;
450     return cfdb->rowdata;
451 }
452 
453 /*****************************************************************************/
454 
CfFetchColumn(CfdbConn * cfdb,int col)455 char *CfFetchColumn(CfdbConn *cfdb, int col)
456 {
457     assert(cfdb != NULL);
458     assert(col >= 0 && (unsigned int) col < cfdb->maxcolumns);
459     if (cfdb->rowdata)
460     {
461         return cfdb->rowdata[col];
462     }
463     else
464     {
465         return NULL;
466     }
467 }
468 
469 /*****************************************************************************/
470 
CfDeleteQuery(CfdbConn * cfdb)471 void CfDeleteQuery(CfdbConn *cfdb)
472 {
473     switch (cfdb->type)
474     {
475     case DATABASE_TYPE_MYSQL:
476         CfDeleteMysqlQuery(cfdb);
477         break;
478 
479     case DATABASE_TYPE_POSTGRES:
480         CfDeletePostgresqlQuery(cfdb);
481         break;
482 
483     default:
484         Log(LOG_LEVEL_VERBOSE, "There is no SQL database selected");
485         break;
486     }
487 
488     if (cfdb->rowdata)
489     {
490         free(cfdb->rowdata);
491         cfdb->rowdata = NULL;
492     }
493 }
494