Track tainted data and refuse to expand it
[exim.git] / src / src / dbfn.c
1 /*************************************************
2 *     Exim - an Internet mail transport agent    *
3 *************************************************/
4
5 /* Copyright (c) University of Cambridge 1995 - 2018 */
6 /* See the file NOTICE for conditions of use and distribution. */
7
8
9 #include "exim.h"
10
11
12 /* Functions for accessing Exim's hints database, which consists of a number of
13 different DBM files. This module does not contain code for reading DBM files
14 for (e.g.) alias expansion. That is all contained within the general search
15 functions. As Exim now has support for several DBM interfaces, all the relevant
16 functions are called as macros.
17
18 All the data in Exim's database is in the nature of *hints*. Therefore it
19 doesn't matter if it gets destroyed by accident. These functions are not
20 supposed to implement a "safe" database.
21
22 Keys are passed in as C strings, and the terminating zero *is* used when
23 building the dbm files. This just makes life easier when scanning the files
24 sequentially.
25
26 Synchronization is required on the database files, and this is achieved by
27 means of locking on independent lock files. (Earlier attempts to lock on the
28 DBM files themselves were never completely successful.) Since callers may in
29 general want to do more than one read or write while holding the lock, there
30 are separate open and close functions. However, the calling modules should
31 arrange to hold the locks for the bare minimum of time. */
32
33
34
35 /*************************************************
36 *         Berkeley DB error callback             *
37 *************************************************/
38
39 /* For Berkeley DB >= 2, we can define a function to be called in case of DB
40 errors. This should help with debugging strange DB problems, e.g. getting "File
41 exists" when you try to open a db file. The API for this function was changed
42 at DB release 4.3. */
43
44 #if defined(USE_DB) && defined(DB_VERSION_STRING)
45 void
46 #if DB_VERSION_MAJOR > 4 || (DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 3)
47 dbfn_bdb_error_callback(const DB_ENV *dbenv, const char *pfx, const char *msg)
48 {
49 dbenv = dbenv;
50 #else
51 dbfn_bdb_error_callback(const char *pfx, char *msg)
52 {
53 #endif
54 pfx = pfx;
55 log_write(0, LOG_MAIN, "Berkeley DB error: %s", msg);
56 }
57 #endif
58
59
60
61
62 /*************************************************
63 *          Open and lock a database file         *
64 *************************************************/
65
66 /* Used for accessing Exim's hints databases.
67
68 Arguments:
69   name     The single-component name of one of Exim's database files.
70   flags    Either O_RDONLY or O_RDWR, indicating the type of open required;
71              O_RDWR implies "create if necessary"
72   dbblock  Points to an open_db block to be filled in.
73   lof      If TRUE, write to the log for actual open failures (locking failures
74            are always logged).
75   panic    If TRUE, panic on failure to create the db directory
76
77 Returns:   NULL if the open failed, or the locking failed. After locking
78            failures, errno is zero.
79
80            On success, dbblock is returned. This contains the dbm pointer and
81            the fd of the locked lock file.
82
83 There are some calls that use O_RDWR|O_CREAT for the flags. Having discovered
84 this in December 2005, I'm not sure if this is correct or not, but for the
85 moment I haven't changed them.
86 */
87
88 open_db *
89 dbfn_open(uschar *name, int flags, open_db *dbblock, BOOL lof, BOOL panic)
90 {
91 int rc, save_errno;
92 BOOL read_only = flags == O_RDONLY;
93 BOOL created = FALSE;
94 flock_t lock_data;
95 uschar dirname[256], filename[256];
96
97 DEBUG(D_hints_lookup) acl_level++;
98
99 /* The first thing to do is to open a separate file on which to lock. This
100 ensures that Exim has exclusive use of the database before it even tries to
101 open it. Early versions tried to lock on the open database itself, but that
102 gave rise to mysterious problems from time to time - it was suspected that some
103 DB libraries "do things" on their open() calls which break the interlocking.
104 The lock file is never written to, but we open it for writing so we can get a
105 write lock if required. If it does not exist, we create it. This is done
106 separately so we know when we have done it, because when running as root we
107 need to change the ownership - see the bottom of this function. We also try to
108 make the directory as well, just in case. We won't be doing this many times
109 unnecessarily, because usually the lock file will be there. If the directory
110 exists, there is no error. */
111
112 snprintf(CS dirname, sizeof(dirname), "%s/db", spool_directory);
113 snprintf(CS filename, sizeof(filename), "%s/%s.lockfile", dirname, name);
114
115 if ((dbblock->lockfd = Uopen(filename, O_RDWR, EXIMDB_LOCKFILE_MODE)) < 0)
116   {
117   created = TRUE;
118   (void)directory_make(spool_directory, US"db", EXIMDB_DIRECTORY_MODE, panic);
119   dbblock->lockfd = Uopen(filename, O_RDWR|O_CREAT, EXIMDB_LOCKFILE_MODE);
120   }
121
122 if (dbblock->lockfd < 0)
123   {
124   log_write(0, LOG_MAIN, "%s",
125     string_open_failed(errno, "database lock file %s", filename));
126   errno = 0;      /* Indicates locking failure */
127   DEBUG(D_hints_lookup) acl_level--;
128   return NULL;
129   }
130
131 /* Now we must get a lock on the opened lock file; do this with a blocking
132 lock that times out. */
133
134 lock_data.l_type = read_only? F_RDLCK : F_WRLCK;
135 lock_data.l_whence = lock_data.l_start = lock_data.l_len = 0;
136
137 DEBUG(D_hints_lookup|D_retry|D_route|D_deliver)
138   debug_printf_indent("locking %s\n", filename);
139
140 sigalrm_seen = FALSE;
141 ALARM(EXIMDB_LOCK_TIMEOUT);
142 rc = fcntl(dbblock->lockfd, F_SETLKW, &lock_data);
143 ALARM_CLR(0);
144
145 if (sigalrm_seen) errno = ETIMEDOUT;
146 if (rc < 0)
147   {
148   log_write(0, LOG_MAIN|LOG_PANIC, "Failed to get %s lock for %s: %s",
149     read_only ? "read" : "write", filename,
150     errno == ETIMEDOUT ? "timed out" : strerror(errno));
151   (void)close(dbblock->lockfd);
152   errno = 0;       /* Indicates locking failure */
153   DEBUG(D_hints_lookup) acl_level--;
154   return NULL;
155   }
156
157 DEBUG(D_hints_lookup) debug_printf_indent("locked  %s\n", filename);
158
159 /* At this point we have an opened and locked separate lock file, that is,
160 exclusive access to the database, so we can go ahead and open it. If we are
161 expected to create it, don't do so at first, again so that we can detect
162 whether we need to change its ownership (see comments about the lock file
163 above.) There have been regular reports of crashes while opening hints
164 databases - often this is caused by non-matching db.h and the library. To make
165 it easy to pin this down, there are now debug statements on either side of the
166 open call. */
167
168 snprintf(CS filename, sizeof(filename), "%s/%s", dirname, name);
169 EXIM_DBOPEN(filename, dirname, flags, EXIMDB_MODE, &(dbblock->dbptr));
170
171 if (!dbblock->dbptr && errno == ENOENT && flags == O_RDWR)
172   {
173   DEBUG(D_hints_lookup)
174     debug_printf_indent("%s appears not to exist: trying to create\n", filename);
175   created = TRUE;
176   EXIM_DBOPEN(filename, dirname, flags|O_CREAT, EXIMDB_MODE, &(dbblock->dbptr));
177   }
178
179 save_errno = errno;
180
181 /* If we are running as root and this is the first access to the database, its
182 files will be owned by root. We want them to be owned by exim. We detect this
183 situation by noting above when we had to create the lock file or the database
184 itself. Because the different dbm libraries use different extensions for their
185 files, I don't know of any easier way of arranging this than scanning the
186 directory for files with the appropriate base name. At least this deals with
187 the lock file at the same time. Also, the directory will typically have only
188 half a dozen files, so the scan will be quick.
189
190 This code is placed here, before the test for successful opening, because there
191 was a case when a file was created, but the DBM library still returned NULL
192 because of some problem. It also sorts out the lock file if that was created
193 but creation of the database file failed. */
194
195 if (created && geteuid() == root_uid)
196   {
197   DIR *dd;
198   struct dirent *ent;
199   uschar *lastname = Ustrrchr(filename, '/') + 1;
200   int namelen = Ustrlen(name);
201
202   *lastname = 0;
203   dd = opendir(CS filename);
204
205   while ((ent = readdir(dd)))
206     if (Ustrncmp(ent->d_name, name, namelen) == 0)
207       {
208       struct stat statbuf;
209       Ustrcpy(lastname, US ent->d_name);
210       if (Ustat(filename, &statbuf) >= 0 && statbuf.st_uid != exim_uid)
211         {
212         DEBUG(D_hints_lookup) debug_printf_indent("ensuring %s is owned by exim\n", filename);
213         if (exim_chown(filename, exim_uid, exim_gid))
214           DEBUG(D_hints_lookup) debug_printf_indent("failed setting %s to owned by exim\n", filename);
215         }
216       }
217
218   closedir(dd);
219   }
220
221 /* If the open has failed, return NULL, leaving errno set. If lof is TRUE,
222 log the event - also for debugging - but debug only if the file just doesn't
223 exist. */
224
225 if (!dbblock->dbptr)
226   {
227   if (lof && save_errno != ENOENT)
228     log_write(0, LOG_MAIN, "%s", string_open_failed(save_errno, "DB file %s",
229         filename));
230   else
231     DEBUG(D_hints_lookup)
232       debug_printf_indent("%s\n", CS string_open_failed(save_errno, "DB file %s",
233           filename));
234   (void)close(dbblock->lockfd);
235   errno = save_errno;
236   DEBUG(D_hints_lookup) acl_level--;
237   return NULL;
238   }
239
240 DEBUG(D_hints_lookup)
241   debug_printf_indent("opened hints database %s: flags=%s\n", filename,
242     flags == O_RDONLY ? "O_RDONLY"
243     : flags == O_RDWR ? "O_RDWR"
244     : flags == (O_RDWR|O_CREAT) ? "O_RDWR|O_CREAT"
245     : "??");
246
247 /* Pass back the block containing the opened database handle and the open fd
248 for the lock. */
249
250 return dbblock;
251 }
252
253
254
255
256 /*************************************************
257 *         Unlock and close a database file       *
258 *************************************************/
259
260 /* Closing a file automatically unlocks it, so after closing the database, just
261 close the lock file.
262
263 Argument: a pointer to an open database block
264 Returns:  nothing
265 */
266
267 void
268 dbfn_close(open_db *dbblock)
269 {
270 EXIM_DBCLOSE(dbblock->dbptr);
271 (void)close(dbblock->lockfd);
272 DEBUG(D_hints_lookup)
273   { debug_printf_indent("closed hints database and lockfile\n"); acl_level--; }
274 }
275
276
277
278
279 /*************************************************
280 *             Read from database file            *
281 *************************************************/
282
283 /* Passing back the pointer unchanged is useless, because there is
284 no guarantee of alignment. Since all the records used by Exim need
285 to be properly aligned to pick out the timestamps, etc., we might as
286 well do the copying centrally here.
287
288 Most calls don't need the length, so there is a macro called dbfn_read which
289 has two arguments; it calls this function adding NULL as the third.
290
291 Arguments:
292   dbblock   a pointer to an open database block
293   key       the key of the record to be read
294   length    a pointer to an int into which to return the length, if not NULL
295
296 Returns: a pointer to the retrieved record, or
297          NULL if the record is not found
298 */
299
300 void *
301 dbfn_read_with_length(open_db *dbblock, const uschar *key, int *length)
302 {
303 void *yield;
304 EXIM_DATUM key_datum, result_datum;
305 int klen = Ustrlen(key) + 1;
306 uschar * key_copy = store_get(klen, is_tainted(key));
307
308 memcpy(key_copy, key, klen);
309
310 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: key=%s\n", key);
311
312 EXIM_DATUM_INIT(key_datum);         /* Some DBM libraries require the datum */
313 EXIM_DATUM_INIT(result_datum);      /* to be cleared before use. */
314 EXIM_DATUM_DATA(key_datum) = CS key_copy;
315 EXIM_DATUM_SIZE(key_datum) = klen;
316
317 if (!EXIM_DBGET(dbblock->dbptr, key_datum, result_datum)) return NULL;
318
319 /* Assume the data store could have been tainted.  Properly, we should
320 store the taint status with the data. */
321
322 yield = store_get(EXIM_DATUM_SIZE(result_datum), TRUE);
323 memcpy(yield, EXIM_DATUM_DATA(result_datum), EXIM_DATUM_SIZE(result_datum));
324 if (length != NULL) *length = EXIM_DATUM_SIZE(result_datum);
325
326 EXIM_DATUM_FREE(result_datum);    /* Some DBM libs require freeing */
327 return yield;
328 }
329
330
331
332 /*************************************************
333 *             Write to database file             *
334 *************************************************/
335
336 /*
337 Arguments:
338   dbblock   a pointer to an open database block
339   key       the key of the record to be written
340   ptr       a pointer to the record to be written
341   length    the length of the record to be written
342
343 Returns:    the yield of the underlying dbm or db "write" function. If this
344             is dbm, the value is zero for OK.
345 */
346
347 int
348 dbfn_write(open_db *dbblock, const uschar *key, void *ptr, int length)
349 {
350 EXIM_DATUM key_datum, value_datum;
351 dbdata_generic *gptr = (dbdata_generic *)ptr;
352 int klen = Ustrlen(key) + 1;
353 uschar * key_copy = store_get(klen, is_tainted(key));
354
355 memcpy(key_copy, key, klen);
356 gptr->time_stamp = time(NULL);
357
358 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_write: key=%s\n", key);
359
360 EXIM_DATUM_INIT(key_datum);         /* Some DBM libraries require the datum */
361 EXIM_DATUM_INIT(value_datum);       /* to be cleared before use. */
362 EXIM_DATUM_DATA(key_datum) = CS key_copy;
363 EXIM_DATUM_SIZE(key_datum) = klen;
364 EXIM_DATUM_DATA(value_datum) = CS ptr;
365 EXIM_DATUM_SIZE(value_datum) = length;
366 return EXIM_DBPUT(dbblock->dbptr, key_datum, value_datum);
367 }
368
369
370
371 /*************************************************
372 *           Delete record from database file     *
373 *************************************************/
374
375 /*
376 Arguments:
377   dbblock    a pointer to an open database block
378   key        the key of the record to be deleted
379
380 Returns: the yield of the underlying dbm or db "delete" function.
381 */
382
383 int
384 dbfn_delete(open_db *dbblock, const uschar *key)
385 {
386 int klen = Ustrlen(key) + 1;
387 uschar * key_copy = store_get(klen, is_tainted(key));
388
389 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_delete: key=%s\n", key);
390
391 memcpy(key_copy, key, klen);
392 EXIM_DATUM key_datum;
393 EXIM_DATUM_INIT(key_datum);         /* Some DBM libraries require clearing */
394 EXIM_DATUM_DATA(key_datum) = CS key_copy;
395 EXIM_DATUM_SIZE(key_datum) = klen;
396 return EXIM_DBDEL(dbblock->dbptr, key_datum);
397 }
398
399
400
401 /*************************************************
402 *         Scan the keys of a database file       *
403 *************************************************/
404
405 /*
406 Arguments:
407   dbblock  a pointer to an open database block
408   start    TRUE if starting a new scan
409            FALSE if continuing with the current scan
410   cursor   a pointer to a pointer to a cursor anchor, for those dbm libraries
411            that use the notion of a cursor
412
413 Returns:   the next record from the file, or
414            NULL if there are no more
415 */
416
417 uschar *
418 dbfn_scan(open_db *dbblock, BOOL start, EXIM_CURSOR **cursor)
419 {
420 EXIM_DATUM key_datum, value_datum;
421 uschar *yield;
422 value_datum = value_datum;    /* dummy; not all db libraries use this */
423
424 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_scan\n");
425
426 /* Some dbm require an initialization */
427
428 if (start) EXIM_DBCREATE_CURSOR(dbblock->dbptr, cursor);
429
430 EXIM_DATUM_INIT(key_datum);         /* Some DBM libraries require the datum */
431 EXIM_DATUM_INIT(value_datum);       /* to be cleared before use. */
432
433 yield = (EXIM_DBSCAN(dbblock->dbptr, key_datum, value_datum, start, *cursor))?
434   US EXIM_DATUM_DATA(key_datum) : NULL;
435
436 /* Some dbm require a termination */
437
438 if (!yield) EXIM_DBDELETE_CURSOR(*cursor);
439 return yield;
440 }
441
442
443
444 /*************************************************
445 **************************************************
446 *             Stand-alone test program           *
447 **************************************************
448 *************************************************/
449
450 #ifdef STAND_ALONE
451
452 int
453 main(int argc, char **cargv)
454 {
455 open_db dbblock[8];
456 int max_db = sizeof(dbblock)/sizeof(open_db);
457 int current = -1;
458 int showtime = 0;
459 int i;
460 dbdata_wait *dbwait = NULL;
461 uschar **argv = USS cargv;
462 uschar buffer[256];
463 uschar structbuffer[1024];
464
465 if (argc != 2)
466   {
467   printf("Usage: test_dbfn directory\n");
468   printf("The subdirectory called \"db\" in the given directory is used for\n");
469   printf("the files used in this test program.\n");
470   return 1;
471   }
472
473 /* Initialize */
474
475 spool_directory = argv[1];
476 debug_selector = D_all - D_memory;
477 debug_file = stderr;
478 big_buffer = malloc(big_buffer_size);
479
480 for (i = 0; i < max_db; i++) dbblock[i].dbptr = NULL;
481
482 printf("\nExim's db functions tester: interface type is %s\n", EXIM_DBTYPE);
483 printf("DBM library: ");
484
485 #ifdef DB_VERSION_STRING
486 printf("Berkeley DB: %s\n", DB_VERSION_STRING);
487 #elif defined(BTREEVERSION) && defined(HASHVERSION)
488   #ifdef USE_DB
489   printf("probably Berkeley DB version 1.8x (native mode)\n");
490   #else
491   printf("probably Berkeley DB version 1.8x (compatibility mode)\n");
492   #endif
493 #elif defined(_DBM_RDONLY) || defined(dbm_dirfno)
494 printf("probably ndbm\n");
495 #elif defined(USE_TDB)
496 printf("using tdb\n");
497 #else
498   #ifdef USE_GDBM
499   printf("probably GDBM (native mode)\n");
500   #else
501   printf("probably GDBM (compatibility mode)\n");
502   #endif
503 #endif
504
505 /* Test the functions */
506
507 printf("\nTest the functions\n> ");
508
509 while (Ufgets(buffer, 256, stdin) != NULL)
510   {
511   int len = Ustrlen(buffer);
512   int count = 1;
513   clock_t start = 1;
514   clock_t stop = 0;
515   uschar *cmd = buffer;
516   while (len > 0 && isspace((uschar)buffer[len-1])) len--;
517   buffer[len] = 0;
518
519   if (isdigit((uschar)*cmd))
520     {
521     count = Uatoi(cmd);
522     while (isdigit((uschar)*cmd)) cmd++;
523     while (isspace((uschar)*cmd)) cmd++;
524     }
525
526   if (Ustrncmp(cmd, "open", 4) == 0)
527     {
528     int i;
529     open_db *odb;
530     uschar *s = cmd + 4;
531     while (isspace((uschar)*s)) s++;
532
533     for (i = 0; i < max_db; i++)
534       if (dbblock[i].dbptr == NULL) break;
535
536     if (i >= max_db)
537       {
538       printf("Too many open databases\n> ");
539       continue;
540       }
541
542     start = clock();
543     odb = dbfn_open(s, O_RDWR, dbblock + i, TRUE, TRUE);
544     stop = clock();
545
546     if (odb)
547       {
548       current = i;
549       printf("opened %d\n", current);
550       }
551     /* Other error cases will have written messages */
552     else if (errno == ENOENT)
553       {
554       printf("open failed: %s%s\n", strerror(errno),
555         #ifdef USE_DB
556         " (or other Berkeley DB error)"
557         #else
558         ""
559         #endif
560         );
561       }
562     }
563
564   else if (Ustrncmp(cmd, "write", 5) == 0)
565     {
566     int rc = 0;
567     uschar *key = cmd + 5;
568     uschar *data;
569
570     if (current < 0)
571       {
572       printf("No current database\n");
573       continue;
574       }
575
576     while (isspace((uschar)*key)) key++;
577     data = key;
578     while (*data != 0 && !isspace((uschar)*data)) data++;
579     *data++ = 0;
580     while (isspace((uschar)*data)) data++;
581
582     dbwait = (dbdata_wait *)(&structbuffer);
583     Ustrcpy(dbwait->text, data);
584
585     start = clock();
586     while (count-- > 0)
587       rc = dbfn_write(dbblock + current, key, dbwait,
588         Ustrlen(data) + sizeof(dbdata_wait));
589     stop = clock();
590     if (rc != 0) printf("Failed: %s\n", strerror(errno));
591     }
592
593   else if (Ustrncmp(cmd, "read", 4) == 0)
594     {
595     uschar *key = cmd + 4;
596     if (current < 0)
597       {
598       printf("No current database\n");
599       continue;
600       }
601     while (isspace((uschar)*key)) key++;
602     start = clock();
603     while (count-- > 0)
604       dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock+ current, key, NULL);
605     stop = clock();
606     printf("%s\n", (dbwait == NULL)? "<not found>" : CS dbwait->text);
607     }
608
609   else if (Ustrncmp(cmd, "delete", 6) == 0)
610     {
611     uschar *key = cmd + 6;
612     if (current < 0)
613       {
614       printf("No current database\n");
615       continue;
616       }
617     while (isspace((uschar)*key)) key++;
618     dbfn_delete(dbblock + current, key);
619     }
620
621   else if (Ustrncmp(cmd, "scan", 4) == 0)
622     {
623     EXIM_CURSOR *cursor;
624     BOOL startflag = TRUE;
625     uschar *key;
626     uschar keybuffer[256];
627     if (current < 0)
628       {
629       printf("No current database\n");
630       continue;
631       }
632     start = clock();
633     while ((key = dbfn_scan(dbblock + current, startflag, &cursor)) != NULL)
634       {
635       startflag = FALSE;
636       Ustrcpy(keybuffer, key);
637       dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock + current,
638         keybuffer, NULL);
639       printf("%s: %s\n", keybuffer, dbwait->text);
640       }
641     stop = clock();
642     printf("End of scan\n");
643     }
644
645   else if (Ustrncmp(cmd, "close", 5) == 0)
646     {
647     uschar *s = cmd + 5;
648     while (isspace((uschar)*s)) s++;
649     i = Uatoi(s);
650     if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n"); else
651       {
652       start = clock();
653       dbfn_close(dbblock + i);
654       stop = clock();
655       dbblock[i].dbptr = NULL;
656       if (i == current) current = -1;
657       }
658     }
659
660   else if (Ustrncmp(cmd, "file", 4) == 0)
661     {
662     uschar *s = cmd + 4;
663     while (isspace((uschar)*s)) s++;
664     i = Uatoi(s);
665     if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n");
666       else current = i;
667     }
668
669   else if (Ustrncmp(cmd, "time", 4) == 0)
670     {
671     showtime = ~showtime;
672     printf("Timing %s\n", showtime? "on" : "off");
673     }
674
675   else if (Ustrcmp(cmd, "q") == 0 || Ustrncmp(cmd, "quit", 4) == 0) break;
676
677   else if (Ustrncmp(cmd, "help", 4) == 0)
678     {
679     printf("close  [<number>]              close file [<number>]\n");
680     printf("delete <key>                   remove record from current file\n");
681     printf("file   <number>                make file <number> current\n");
682     printf("open   <name>                  open db file\n");
683     printf("q[uit]                         exit program\n");
684     printf("read   <key>                   read record from current file\n");
685     printf("scan                           scan current file\n");
686     printf("time                           time display on/off\n");
687     printf("write  <key> <rest-of-line>    write record to current file\n");
688     }
689
690   else printf("Eh?\n");
691
692   if (showtime && stop >= start)
693     printf("start=%d stop=%d difference=%d\n", (int)start, (int)stop,
694      (int)(stop - start));
695
696   printf("> ");
697   }
698
699 for (i = 0; i < max_db; i++)
700   {
701   if (dbblock[i].dbptr != NULL)
702     {
703     printf("\nClosing %d", i);
704     dbfn_close(dbblock + i);
705     }
706   }
707
708 printf("\n");
709 return 0;
710 }
711
712 #endif
713
714 /* End of dbfn.c */