SPDX: license tags (mostly by guesswork)
[exim.git] / src / src / lookups / ibase.c
1 /*************************************************
2 *     Exim - an Internet mail transport agent    *
3 *************************************************/
4
5 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
6 /* Copyright (c) University of Cambridge 1995 - 2018 */
7 /* See the file NOTICE for conditions of use and distribution. */
8 /* SPDX-License-Identifier: GPL-2.0-only */
9
10 /* The code in this module was contributed by Ard Biesheuvel. */
11
12 #include "../exim.h"
13 #include "lf_functions.h"
14
15 #include <ibase.h>              /* The system header */
16
17 /* Structure and anchor for caching connections. */
18
19 typedef struct ibase_connection {
20     struct ibase_connection *next;
21     uschar *server;
22     isc_db_handle dbh;
23     isc_tr_handle transh;
24 } ibase_connection;
25
26 static ibase_connection *ibase_connections = NULL;
27
28
29
30 /*************************************************
31 *              Open entry point                  *
32 *************************************************/
33
34 /* See local README for interface description. */
35
36 static void *ibase_open(const uschar * filename, uschar ** errmsg)
37 {
38 return (void *) (1);        /* Just return something non-null */
39 }
40
41
42
43 /*************************************************
44 *               Tidy entry point                 *
45 *************************************************/
46
47 /* See local README for interface description. */
48
49 static void ibase_tidy(void)
50 {
51     ibase_connection *cn;
52     ISC_STATUS status[20];
53
54     while ((cn = ibase_connections) != NULL) {
55         ibase_connections = cn->next;
56         DEBUG(D_lookup) debug_printf_indent("close Interbase connection: %s\n",
57                                      cn->server);
58         isc_commit_transaction(status, &cn->transh);
59         isc_detach_database(status, &cn->dbh);
60     }
61 }
62
63 static int fetch_field(char *buffer, int buffer_size, XSQLVAR * var)
64 {
65     if (buffer_size < var->sqllen)
66         return 0;
67
68     switch (var->sqltype & ~1) {
69     case SQL_VARYING:
70         strncpy(buffer, &var->sqldata[2], *(short *) var->sqldata);
71         return *(short *) var->sqldata;
72     case SQL_TEXT:
73         strncpy(buffer, var->sqldata, var->sqllen);
74         return var->sqllen;
75     case SQL_SHORT:
76         return sprintf(buffer, "%d", *(short *) var->sqldata);
77     case SQL_LONG:
78         return sprintf(buffer, "%ld", *(ISC_LONG *) var->sqldata);
79 #ifdef SQL_INT64
80     case SQL_INT64:
81         return sprintf(buffer, "%lld", *(ISC_INT64 *) var->sqldata);
82 #endif
83     default:
84         /* not implemented */
85         return 0;
86     }
87 }
88
89 /*************************************************
90 *        Internal search function                *
91 *************************************************/
92
93 /* This function is called from the find entry point to do the search for a
94 single server.
95
96 Arguments:
97   query        the query string
98   server       the server string
99   resultptr    where to store the result
100   errmsg       where to point an error message
101   defer_break  TRUE if no more servers are to be tried after DEFER
102
103 The server string is of the form "host:dbname|user|password". The host can be
104 host:port. This string is in a nextinlist temporary buffer, so can be
105 overwritten.
106
107 Returns:       OK, FAIL, or DEFER
108 */
109
110 static int
111 perform_ibase_search(uschar * query, uschar * server, uschar ** resultptr,
112                      uschar ** errmsg, BOOL * defer_break)
113 {
114 isc_stmt_handle stmth = NULL;
115 XSQLDA *out_sqlda;
116 XSQLVAR *var;
117 int i;
118 rmark reset_point;
119
120 char buffer[256];
121 ISC_STATUS status[20], *statusp = status;
122
123 gstring * result;
124 int yield = DEFER;
125 ibase_connection *cn;
126 uschar *server_copy = NULL;
127 uschar *sdata[3];
128
129 /* Disaggregate the parameters from the server argument. The order is host,
130 database, user, password. We can write to the string, since it is in a
131 nextinlist temporary buffer. The copy of the string that is used for caching
132 has the password removed. This copy is also used for debugging output. */
133
134 for (int i = 2; i > 0; i--)
135   {
136   uschar *pp = Ustrrchr(server, '|');
137
138   if (pp == NULL)
139     {
140     *errmsg = string_sprintf("incomplete Interbase server data: %s",
141                        (i == 3) ? server : server_copy);
142     *defer_break = TRUE;
143     return DEFER;
144     }
145   *pp++ = 0;
146   sdata[i] = pp;
147   if (i == 2)
148       server_copy = string_copy(server);   /* sans password */
149   }
150 sdata[0] = server;          /* What's left at the start */
151
152 /* See if we have a cached connection to the server */
153
154 for (cn = ibase_connections; cn != NULL; cn = cn->next)
155   if (Ustrcmp(cn->server, server_copy) == 0)
156     break;
157
158 /* Use a previously cached connection ? */
159
160 if (cn)
161   {
162   static char db_info_options[] = { isc_info_base_level };
163
164   /* test if the connection is alive */
165   if (isc_database_info(status, &cn->dbh, sizeof(db_info_options),
166         db_info_options, sizeof(buffer), buffer))
167     {
168     /* error occurred: assume connection is down */
169     DEBUG(D_lookup)
170         debug_printf
171         ("Interbase cleaning up cached connection: %s\n",
172          cn->server);
173     isc_detach_database(status, &cn->dbh);
174     }
175   else
176     DEBUG(D_lookup) debug_printf_indent("Interbase using cached connection for %s\n",
177                      server_copy);
178   }
179 else
180   {
181   cn = store_get(sizeof(ibase_connection), GET_UNTAINTED);
182   cn->server = server_copy;
183   cn->dbh = NULL;
184   cn->transh = NULL;
185   cn->next = ibase_connections;
186   ibase_connections = cn;
187   }
188
189 /* If no cached connection, we must set one up. */
190
191 if (cn->dbh == NULL || cn->transh == NULL)
192   {
193   char *dpb;
194   short dpb_length;
195   static char trans_options[] =
196       { isc_tpb_version3, isc_tpb_read, isc_tpb_read_committed,
197       isc_tpb_rec_version
198   };
199
200   /* Construct the database parameter buffer. */
201   dpb = buffer;
202   *dpb++ = isc_dpb_version1;
203   *dpb++ = isc_dpb_user_name;
204   *dpb++ = strlen(sdata[1]);
205   for (char * p = sdata[1]; *p;)
206       *dpb++ = *p++;
207   *dpb++ = isc_dpb_password;
208   *dpb++ = strlen(sdata[2]);
209   for (char * p = sdata[2]; *p;)
210       *dpb++ = *p++;
211   dpb_length = dpb - buffer;
212
213   DEBUG(D_lookup)
214       debug_printf_indent("new Interbase connection: database=%s user=%s\n",
215                    sdata[0], sdata[1]);
216
217   /* Connect to the database */
218   if (isc_attach_database
219       (status, 0, sdata[0], &cn->dbh, dpb_length, buffer))
220     {
221     isc_interprete(buffer, &statusp);
222     *errmsg =
223         string_sprintf("Interbase attach() failed: %s", buffer);
224     *defer_break = FALSE;
225     goto IBASE_EXIT;
226     }
227
228   /* Now start a read-only read-committed transaction */
229   if (isc_start_transaction
230       (status, &cn->transh, 1, &cn->dbh, sizeof(trans_options),
231        trans_options))
232     {
233     isc_interprete(buffer, &statusp);
234     isc_detach_database(status, &cn->dbh);
235     *errmsg =
236         string_sprintf("Interbase start_transaction() failed: %s",
237                        buffer);
238     *defer_break = FALSE;
239     goto IBASE_EXIT;
240     }
241   }
242
243 /* Run the query */
244 if (isc_dsql_allocate_statement(status, &cn->dbh, &stmth))
245   {
246   isc_interprete(buffer, &statusp);
247   *errmsg =
248       string_sprintf("Interbase alloc_statement() failed: %s",
249                      buffer);
250   *defer_break = FALSE;
251   goto IBASE_EXIT;
252   }
253
254 /* Lacking any information, assume that the data is untainted */
255 reset_point = store_mark();
256 out_sqlda = store_get(XSQLDA_LENGTH(1), GET_UNTAINTED);
257 out_sqlda->version = SQLDA_VERSION1;
258 out_sqlda->sqln = 1;
259
260 if (isc_dsql_prepare
261     (status, &cn->transh, &stmth, 0, query, 1, out_sqlda))
262   {
263   isc_interprete(buffer, &statusp);
264   reset_point = store_reset(reset_point);
265   out_sqlda = NULL;
266   *errmsg =
267       string_sprintf("Interbase prepare_statement() failed: %s",
268                      buffer);
269   *defer_break = FALSE;
270   goto IBASE_EXIT;
271   }
272
273 /* re-allocate the output structure if there's more than one field */
274 if (out_sqlda->sqln < out_sqlda->sqld)
275   {
276   XSQLDA *new_sqlda = store_get(XSQLDA_LENGTH(out_sqlda->sqld), GET_UNTAINTED);
277   if (isc_dsql_describe
278       (status, &stmth, out_sqlda->version, new_sqlda))
279     {
280     isc_interprete(buffer, &statusp);
281     isc_dsql_free_statement(status, &stmth, DSQL_drop);
282     reset_point = store_reset(reset_point);
283     out_sqlda = NULL;
284     *errmsg = string_sprintf("Interbase describe_statement() failed: %s",
285                        buffer);
286     *defer_break = FALSE;
287     goto IBASE_EXIT;
288     }
289   out_sqlda = new_sqlda;
290   }
291
292 /* allocate storage for every returned field */
293 for (i = 0, var = out_sqlda->sqlvar; i < out_sqlda->sqld; i++, var++)
294   {
295   switch (var->sqltype & ~1)
296     {
297     case SQL_VARYING:
298         var->sqldata = CS store_get(sizeof(char) * var->sqllen + 2, GET_UNTAINTED);
299         break;
300     case SQL_TEXT:
301         var->sqldata = CS store_get(sizeof(char) * var->sqllen, GET_UNTAINTED);
302         break;
303     case SQL_SHORT:
304         var->sqldata = CS  store_get(sizeof(short), GET_UNTAINTED);
305         break;
306     case SQL_LONG:
307         var->sqldata = CS  store_get(sizeof(ISC_LONG), GET_UNTAINTED);
308         break;
309 #ifdef SQL_INT64
310     case SQL_INT64:
311         var->sqldata = CS  store_get(sizeof(ISC_INT64), GET_UNTAINTED);
312         break;
313 #endif
314     case SQL_FLOAT:
315         var->sqldata = CS  store_get(sizeof(float), GET_UNTAINTED);
316         break;
317     case SQL_DOUBLE:
318         var->sqldata = CS  store_get(sizeof(double), GET_UNTAINTED);
319         break;
320 #ifdef SQL_TIMESTAMP
321     case SQL_DATE:
322         var->sqldata = CS  store_get(sizeof(ISC_QUAD), GET_UNTAINTED);
323         break;
324 #else
325     case SQL_TIMESTAMP:
326         var->sqldata = CS  store_get(sizeof(ISC_TIMESTAMP), GET_UNTAINTED);
327         break;
328     case SQL_TYPE_DATE:
329         var->sqldata = CS  store_get(sizeof(ISC_DATE), GET_UNTAINTED);
330         break;
331     case SQL_TYPE_TIME:
332         var->sqldata = CS  store_get(sizeof(ISC_TIME), GET_UNTAINTED);
333         break;
334   #endif
335     }
336   if (var->sqltype & 1)
337     var->sqlind = (short *) store_get(sizeof(short), GET_UNTAINTED);
338   }
339
340 /* finally, we're ready to execute the statement */
341 if (isc_dsql_execute
342     (status, &cn->transh, &stmth, out_sqlda->version, NULL))
343   {
344   isc_interprete(buffer, &statusp);
345   *errmsg = string_sprintf("Interbase describe_statement() failed: %s",
346                      buffer);
347   isc_dsql_free_statement(status, &stmth, DSQL_drop);
348   *defer_break = FALSE;
349   goto IBASE_EXIT;
350   }
351
352 while (isc_dsql_fetch(status, &stmth, out_sqlda->version, out_sqlda) != 100L)
353   {
354   /* check if an error occurred */
355   if (status[0] & status[1])
356     {
357     isc_interprete(buffer, &statusp);
358     *errmsg =
359         string_sprintf("Interbase fetch() failed: %s", buffer);
360     isc_dsql_free_statement(status, &stmth, DSQL_drop);
361     *defer_break = FALSE;
362     goto IBASE_EXIT;
363     }
364
365   if (result)
366     result = string_catn(result, US "\n", 1);
367
368   /* Find the number of fields returned. If this is one, we don't add field
369      names to the data. Otherwise we do. */
370   if (out_sqlda->sqld == 1)
371     {
372     if (out_sqlda->sqlvar[0].sqlind == NULL || *out_sqlda->sqlvar[0].sqlind != -1)     /* NULL value yields nothing */
373       result = string_catn(result, US buffer,
374                        fetch_field(buffer, sizeof(buffer),
375                                    &out_sqlda->sqlvar[0]));
376     }
377
378   else
379     for (int i = 0; i < out_sqlda->sqld; i++)
380       {
381       int len = fetch_field(buffer, sizeof(buffer), &out_sqlda->sqlvar[i]);
382
383       result = string_catn(result, US out_sqlda->sqlvar[i].aliasname,
384                      out_sqlda->sqlvar[i].aliasname_length);
385       result = string_catn(result, US "=", 1);
386
387       /* Quote the value if it contains spaces or is empty */
388
389       if (*out_sqlda->sqlvar[i].sqlind == -1)       /* NULL value */
390         result = string_catn(result, US "\"\"", 2);
391
392       else if (buffer[0] == 0 || Ustrchr(buffer, ' ') != NULL)
393         {
394         result = string_catn(result, US "\"", 1);
395         for (int j = 0; j < len; j++)
396           {
397           if (buffer[j] == '\"' || buffer[j] == '\\')
398               result = string_cat(result, US "\\", 1);
399           result = string_cat(result, US buffer + j, 1);
400           }
401         result = string_catn(result, US "\"", 1);
402         }
403       else
404         result = string_catn(result, US buffer, len);
405       result = string_catn(result, US " ", 1);
406       }
407   }
408
409 /* If result is NULL then no data has been found and so we return FAIL.
410 Otherwise, we must terminate the string which has been built; string_cat()
411 always leaves enough room for a terminating zero. */
412
413 if (!result)
414   {
415   yield = FAIL;
416   *errmsg = US "Interbase: no data found";
417   }
418 else
419   gstring_release_unused(result);
420
421
422 /* Get here by goto from various error checks. */
423
424 IBASE_EXIT:
425
426 if (stmth)
427   isc_dsql_free_statement(status, &stmth, DSQL_drop);
428
429 /* Non-NULL result indicates a successful result */
430
431 if (result)
432   {
433   *resultptr = string_from_gstring(result);
434   return OK;
435   }
436 else
437   {
438   DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
439   return yield;           /* FAIL or DEFER */
440   }
441 }
442
443
444
445
446 /*************************************************
447 *               Find entry point                 *
448 *************************************************/
449
450 /* See local README for interface description. The handle and filename
451 arguments are not used. Loop through a list of servers while the query is
452 deferred with a retryable error. */
453
454 static int
455 ibase_find(void * handle, const uschar * filename, uschar * query, int length,
456   uschar ** result, uschar ** errmsg, uint * do_cache, const uschar * opts)
457 {
458 int sep = 0;
459 uschar *server;
460 uschar *list = ibase_servers;
461
462 DEBUG(D_lookup) debug_printf_indent("Interbase query: %s\n", query);
463
464 while ((server = string_nextinlist(&list, &sep, NULL, 0)))
465   {
466   BOOL defer_break = FALSE;
467   int rc = perform_ibase_search(query, server, result, errmsg, &defer_break);
468   if (rc != DEFER || defer_break)
469     return rc;
470   }
471
472 if (!ibase_servers)
473   *errmsg = US "no Interbase servers defined (ibase_servers option)";
474
475 return DEFER;
476 }
477
478
479
480 /*************************************************
481 *               Quote entry point                *
482 *************************************************/
483
484 /* The only characters that need to be quoted (with backslash) are newline,
485 tab, carriage return, backspace, backslash itself, and the quote characters.
486 Percent, and underscore and not escaped. They are only special in contexts
487 where they can be wild cards, and this isn't usually the case for data inserted
488 from messages, since that isn't likely to be treated as a pattern of any kind.
489 Sadly, MySQL doesn't seem to behave like other programs. If you use something
490 like "where id="ab\%cd" it does not treat the string as "ab%cd". So you really
491 can't quote "on spec".
492
493 Arguments:
494   s          the string to be quoted
495   opt        additional option text or NULL if none
496   idx        lookup type index
497
498 Returns:     the processed string or NULL for a bad option
499 */
500
501 static uschar *
502 ibase_quote(uschar * s, uschar * opt, unsigned idx)
503 {
504 int c;
505 int count = 0;
506 uschar * t = s, * quoted;
507
508 if (opt)
509   return NULL;            /* No options recognized */
510
511 while ((c = *t++))
512   if (c == '\'') count++;
513
514 t = quoted = store_get_quoted(Ustrlen(s) + count + 1, s, idx);
515
516 while ((c = *s++))
517   if (c == '\'') { *t++ = '\''; *t++ = '\''; }
518   else *t++ = c;
519
520 *t = 0;
521 return quoted;
522 }
523
524
525 /*************************************************
526 *         Version reporting entry point          *
527 *************************************************/
528
529 /* See local README for interface description. */
530
531 #include "../version.h"
532
533 gstring *
534 ibase_version_report(gstring * g)
535 {
536 #ifdef DYNLOOKUP
537 g = string_fmt_append(g, "Library version: ibase: Exim version %s\n", EXIM_VERSION_STR));
538 #endif
539 return g;
540 }
541
542
543 static lookup_info _lookup_info = {
544   .name = US"ibase",                    /* lookup name */
545   .type = lookup_querystyle,            /* query-style lookup */
546   .open = ibase_open,                   /* open function */
547   .check NULL,                          /* no check function */
548   .find = ibase_find,                   /* find function */
549   .close = NULL,                        /* no close function */
550   .tidy = ibase_tidy,                   /* tidy function */
551   .quote = ibase_quote,                 /* quoting function */
552   .version_report = ibase_version_report           /* version reporting */
553 };
554
555 #ifdef DYNLOOKUP
556 #define ibase_lookup_module_info _lookup_module_info
557 #endif
558
559 static lookup_info *_lookup_list[] = { &_lookup_info };
560 lookup_module_info ibase_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
561
562 /* End of lookups/ibase.c */