Faffing around with tests.
[users/jgh/exim.git] / test / src / fakens.c
1 /* $Cambridge: exim/test/src/fakens.c,v 1.2 2006/02/16 10:05:34 ph10 Exp $ */
2
3 /*************************************************
4 *       fakens - A Fake Nameserver Program       *
5 *************************************************/
6
7 /* This program exists to support the testing of DNS handling code in Exim. It
8 avoids the need to install special zones in a real nameserver. When Exim is
9 running in its (new) test harness, DNS lookups are first passed to this program
10 instead of to the real resolver. (With a few exceptions - see the discussion in
11 the test suite's README file.) The program is also passed the name of the Exim
12 spool directory; it expects to find its "zone files" in ../dnszones relative to
13 that directory. Note that there is little checking in this program. The fake
14 zone files are assumed to be syntactically valid.
15
16 The zones that are handled are found by scanning the dnszones directory. A file
17 whose name is of the form db.ip4.x is a zone file for .x.in-addr.arpa; a file
18 whose name is of the form db.ip6.x is a zone file for .x.ip6.arpa; a file of
19 the form db.anything.else is a zone file for .anything.else. A file of the form
20 qualify.x.y specifies the domain that is used to qualify single-component
21 names, except for the name "dontqualify".
22
23 The arguments to the program are:
24
25   the name of the Exim spool directory
26   the domain name that is being sought
27   the DNS record type that is being sought
28
29 The output from the program is written to stdout. It is supposed to be in
30 exactly the same format as a traditional namserver response (see RFC 1035) so
31 that Exim can process it as normal. At present, no compression is used.
32 Error messages are written to stderr.
33
34 The return codes from the program are zero for success, and otherwise the
35 values that are set in h_errno after a failing call to the normal resolver:
36
37   1 HOST_NOT_FOUND     host not found (authoritative)
38   2 TRY_AGAIN          server failure
39   3 NO_RECOVERY        non-recoverable error
40   4 NO_DATA            valid name, no data of requested type
41
42 In a real nameserver, TRY_AGAIN is also used for a non-authoritative not found,
43 but it is not used for that here. There is also one extra return code:
44
45   5 PASS_ON            requests Exim to call res_search()
46
47 This is used for zones that fakens does not recognize. It is also used if a
48 line in the zone file contains exactly this:
49
50   PASS ON NOT FOUND
51
52 and the domain is not found. It converts the the result to PASS_ON instead of
53 HOST_NOT_FOUND. */
54
55 #include <ctype.h>
56 #include <stdarg.h>
57 #include <stdio.h>
58 #include <string.h>
59 #include <netdb.h>
60 #include <errno.h>
61 #include <arpa/nameser.h>
62 #include <sys/types.h>
63 #include <dirent.h>
64
65 #define FALSE         0
66 #define TRUE          1
67 #define PASS_ON       5
68
69 typedef int BOOL;
70 typedef unsigned char uschar;
71
72 #define CS   (char *)
73 #define CCS  (const char *)
74 #define US   (unsigned char *)
75
76 #define Ustrcat(s,t)       strcat(CS(s),CCS(t))
77 #define Ustrchr(s,n)       US strchr(CCS(s),n)
78 #define Ustrcmp(s,t)       strcmp(CCS(s),CCS(t))
79 #define Ustrcpy(s,t)       strcpy(CS(s),CCS(t))
80 #define Ustrlen(s)         (int)strlen(CCS(s))
81 #define Ustrncmp(s,t,n)    strncmp(CCS(s),CCS(t),n)
82 #define Ustrncpy(s,t,n)    strncpy(CS(s),CCS(t),n)
83
84 typedef struct zoneitem {
85   uschar *zone;
86   uschar *zonefile;
87 } zoneitem;
88
89 typedef struct tlist {
90   uschar *name;
91   int value;
92 } tlist;
93
94 /* On some (older?) operating systems, the standard ns_t_xxx definitions are
95 not available, and only the older T_xxx ones exist in nameser.h. If ns_t_a is
96 not defined, assume we are in this state. A really old system might not even
97 know about AAAA and SRV at all. */
98
99 #ifndef ns_t_a
100 #define ns_t_a      T_A
101 #define ns_t_ns     T_NS
102 #define ns_t_cname  T_CNAME
103 #define ns_t_soa    T_SOA
104 #define ns_t_ptr    T_PTR
105 #define ns_t_mx     T_MX
106 #define ns_t_txt    T_TXT
107 #define ns_t_aaaa   T_AAAA
108 #define ns_t_srv    T_SRV
109 #ifndef T_AAAA
110 #define T_AAAA      28
111 #endif
112 #ifndef T_SRV
113 #define T_SRV       33
114 #endif
115 #endif
116
117 static tlist type_list[] = {
118   { US"A",       ns_t_a },
119   { US"NS",      ns_t_ns },
120   { US"CNAME",   ns_t_cname },
121 /*  { US"SOA",     ns_t_soa },  Not currently in use */
122   { US"PTR",     ns_t_ptr },
123   { US"MX",      ns_t_mx },
124   { US"TXT",     ns_t_txt },
125   { US"AAAA",    ns_t_aaaa },
126   { US"SRV",     ns_t_srv },
127   { NULL,        0 }
128 };
129
130
131
132 /*************************************************
133 *           Get memory and sprintf into it       *
134 *************************************************/
135
136 /* This is used when building a table of zones and their files.
137
138 Arguments:
139   format       a format string
140   ...          arguments
141
142 Returns:       pointer to formatted string
143 */
144
145 static uschar *
146 fcopystring(uschar *format, ...)
147 {
148 uschar *yield;
149 char buffer[256];
150 va_list ap;
151 va_start(ap, format);
152 vsprintf(buffer, format, ap);
153 va_end(ap);
154 yield = (uschar *)malloc(Ustrlen(buffer) + 1);
155 Ustrcpy(yield, buffer);
156 return yield;
157 }
158
159
160 /*************************************************
161 *             Pack name into memory              *
162 *************************************************/
163
164 /* This function packs a domain name into memory according to DNS rules. At
165 present, it doesn't do any compression.
166
167 Arguments:
168   name         the name
169   pk           where to put it
170
171 Returns:       the updated value of pk
172 */
173
174 static uschar *
175 packname(uschar *name, uschar *pk)
176 {
177 while (*name != 0)
178   {
179   uschar *p = name;
180   while (*p != 0 && *p != '.') p++;
181   *pk++ = (p - name);
182   memmove(pk, name, p - name);
183   pk += p - name;
184   name = (*p == 0)? p : p + 1;
185   }
186 *pk++ = 0;
187 return pk;
188 }
189
190
191
192 /*************************************************
193 *              Scan file for RRs                 *
194 *************************************************/
195
196 /* This function scans an open "zone file" for appropriate records, and adds
197 any that are found to the output buffer.
198
199 Arguments:
200   f           the input FILE
201   zone        the current zone name
202   domain      the domain we are looking for
203   qtype       the type of RR we want
204   qtypelen    the length of qtype
205   pkptr       points to the output buffer pointer; this is updated
206   countptr    points to the record count; this is updated
207
208 Returns:      0 on success, else HOST_NOT_FOUND or NO_DATA or NO_RECOVERY or
209               PASS_ON - the latter if a "PASS ON NOT FOUND" line is seen
210 */
211
212 static int
213 find_records(FILE *f, uschar *zone, uschar *domain, uschar *qtype,
214   int qtypelen, uschar **pkptr, int *countptr)
215 {
216 int yield = HOST_NOT_FOUND;
217 int domainlen = Ustrlen(domain);
218 BOOL pass_on_not_found = FALSE;
219 tlist *typeptr;
220 uschar *pk = *pkptr;
221 uschar buffer[256];
222 uschar rrdomain[256];
223 uschar RRdomain[256];
224
225 /* Decode the required type */
226
227 for (typeptr = type_list; typeptr->name != NULL; typeptr++)
228   { if (Ustrcmp(typeptr->name, qtype) == 0) break; }
229 if (typeptr->name == NULL)
230   {
231   fprintf(stderr, "fakens: unknown record type %s\n", qtype);
232   return NO_RECOVERY;
233   }
234
235 rrdomain[0] = 0;                 /* No previous domain */
236 (void)fseek(f, 0, SEEK_SET);     /* Start again at the beginning */
237
238 /* Scan for RRs */
239
240 while (fgets(CS buffer, sizeof(buffer), f) != NULL)
241   {
242   uschar *rdlptr;
243   uschar *p, *ep, *pp;
244   BOOL found_cname = FALSE;
245   int i, plen, value;
246   int tvalue = typeptr->value;
247   int qtlen = qtypelen;
248
249   p = buffer;
250   while (isspace(*p)) p++;
251   if (*p == 0 || *p == ';') continue;
252
253   if (Ustrncmp(p, "PASS ON NOT FOUND", 17) == 0)
254     {
255     pass_on_not_found = TRUE;
256     continue;
257     }
258
259   ep = buffer + Ustrlen(buffer);
260   while (isspace(ep[-1])) ep--;
261   *ep = 0;
262
263   p = buffer;
264   if (!isspace(*p))
265     {
266     uschar *pp = rrdomain;
267     uschar *PP = RRdomain;
268     while (!isspace(*p))
269       {
270       *pp++ = tolower(*p);
271       *PP++ = *p++;
272       }
273     if (pp[-1] != '.')
274       {
275       Ustrcpy(pp, zone);
276       Ustrcpy(PP, zone);
277       }
278     else
279       {
280       pp[-1] = 0;
281       PP[-1] = 0;
282       }
283     }
284
285   /* Compare domain names; first check for a wildcard */
286
287   if (rrdomain[0] == '*')
288     {
289     int restlen = Ustrlen(rrdomain) - 1;
290     if (domainlen > restlen &&
291         Ustrcmp(domain + domainlen - restlen, rrdomain + 1) != 0) continue;
292     }
293
294   /* Not a wildcard RR */
295
296   else if (Ustrcmp(domain, rrdomain) != 0) continue;
297
298   /* The domain matches */
299
300   if (yield == HOST_NOT_FOUND) yield = NO_DATA;
301
302   /* Compare RR types; a CNAME record is always returned */
303
304   while (isspace(*p)) p++;
305
306   if (Ustrncmp(p, "CNAME", 5) == 0)
307     {
308     tvalue = ns_t_cname;
309     qtlen = 5;
310     found_cname = TRUE;
311     }
312   else if (Ustrncmp(p, qtype, qtypelen) != 0 || !isspace(p[qtypelen])) continue;
313
314   /* Found a relevant record */
315
316   yield = 0;
317   *countptr = *countptr + 1;
318
319   p += qtlen;
320   while (isspace(*p)) p++;
321
322   /* For a wildcard record, use the search name; otherwise use the record's
323   name in its original case because it might contain upper case letters. */
324
325   pk = packname((rrdomain[0] == '*')? domain : RRdomain, pk);
326   *pk++ = (tvalue >> 8) & 255;
327   *pk++ = (tvalue) & 255;
328   *pk++ = 0;
329   *pk++ = 1;     /* class = IN */
330
331   pk += 4;       /* TTL field; don't care */
332
333   rdlptr = pk;   /* remember rdlength field */
334   pk += 2;
335
336   /* The rest of the data depends on the type */
337
338   switch (tvalue)
339     {
340     case ns_t_soa:  /* Not currently used */
341     break;
342
343     case ns_t_a:
344     for (i = 0; i < 4; i++)
345       {
346       value = 0;
347       while (isdigit(*p)) value = value*10 + *p++ - '0';
348       *pk++ = value;
349       p++;
350       }
351     break;
352
353     /* The only occurrence of a double colon is for ::1 */
354     case ns_t_aaaa:
355     if (Ustrcmp(p, "::1") == 0)
356       {
357       memset(pk, 0, 15);
358       pk += 15;
359       *pk++ = 1;
360       }
361     else for (i = 0; i < 8; i++)
362       {
363       value = 0;
364       while (isxdigit(*p))
365         {
366         value = value * 16 + toupper(*p) - (isdigit(*p)? '0' : '7');
367         p++;
368         }
369       *pk++ = (value >> 8) & 255;
370       *pk++ = value & 255;
371       p++;
372       }
373     break;
374
375     case ns_t_mx:
376     value = 0;
377     while (isdigit(*p)) value = value*10 + *p++ - '0';
378     while (isspace(*p)) p++;
379     *pk++ = (value >> 8) & 255;
380     *pk++ = value & 255;
381     if (ep[-1] != '.') sprintf(ep, "%s.", zone);
382     pk = packname(p, pk);
383     plen = Ustrlen(p);
384     break;
385
386     case ns_t_txt:
387     pp = pk++;
388     if (*p == '"') p++;   /* Should always be the case */
389     while (*p != 0 && *p != '"') *pk++ = *p++;
390     *pp = pk - pp - 1;
391     break;
392
393     case ns_t_srv:
394     for (i = 0; i < 3; i++)
395       {
396       value = 0;
397       while (isdigit(*p)) value = value*10 + *p++ - '0';
398       while (isspace(*p)) p++;
399       *pk++ = (value >> 8) & 255;
400       *pk++ = value & 255;
401       }
402
403     /* Fall through */
404
405     case ns_t_cname:
406     case ns_t_ns:
407     case ns_t_ptr:
408     if (ep[-1] != '.') sprintf(ep, "%s.", zone);
409     pk = packname(p, pk);
410     plen = Ustrlen(p);
411     break;
412     }
413
414   /* Fill in the length, and we are done with this RR */
415
416   rdlptr[0] = ((pk - rdlptr - 2) >> 8) & 255;
417   rdlptr[1] = (pk -rdlptr - 2) & 255;
418
419   /* If we have just yielded a CNAME, we must change the domain name to the
420   new domain, and re-start the scan from the beginning. */
421
422   if (found_cname)
423     {
424     domain = fcopystring("%s", p);
425     domainlen = Ustrlen(domain);
426     domain[domainlen - 1] = 0;       /* Removed trailing dot */
427     rrdomain[0] = 0;                 /* No previous domain */
428     (void)fseek(f, 0, SEEK_SET);     /* Start again at the beginning */
429     }
430   }
431
432 *pkptr = pk;
433 return (yield == HOST_NOT_FOUND && pass_on_not_found)? PASS_ON : yield;
434 }
435
436
437
438 /*************************************************
439 *           Entry point and main program         *
440 *************************************************/
441
442 int
443 main(int argc, char **argv)
444 {
445 FILE *f;
446 DIR *d;
447 int domlen, qtypelen;
448 int yield, count;
449 int i;
450 int zonecount = 0;
451 struct dirent *de;
452 zoneitem zones[32];
453 uschar *qualify = NULL;
454 uschar *p, *zone;
455 uschar *zonefile = NULL;
456 uschar domain[256];
457 uschar buffer[256];
458 uschar qtype[12];
459 uschar packet[512];
460 uschar *pk = packet;
461
462 if (argc != 4)
463   {
464   fprintf(stderr, "fakens: expected 3 arguments, received %d\n", argc-1);
465   return NO_RECOVERY;
466   }
467
468 /* Find the zones */
469
470 (void)sprintf(buffer, "%s/../dnszones", argv[1]);
471
472 d = opendir(CCS buffer);
473 if (d == NULL)
474   {
475   fprintf(stderr, "fakens: failed to opendir %s: %s\n", buffer,
476     strerror(errno));
477   return NO_RECOVERY;
478   }
479
480 while ((de = readdir(d)) != NULL)
481   {
482   uschar *name = de->d_name;
483   if (Ustrncmp(name, "qualify.", 8) == 0)
484     {
485     qualify = fcopystring("%s", name + 7);
486     continue;
487     }
488   if (Ustrncmp(name, "db.", 3) != 0) continue;
489   if (Ustrncmp(name + 3, "ip4.", 4) == 0)
490     zones[zonecount].zone = fcopystring("%s.in-addr.arpa", name + 6);
491   else if (Ustrncmp(name + 3, "ip6.", 4) == 0)
492     zones[zonecount].zone = fcopystring("%s.ip6.arpa", name + 6);
493   else
494     zones[zonecount].zone = fcopystring("%s", name + 2);
495   zones[zonecount++].zonefile = fcopystring("%s", name);
496   }
497 (void)closedir(d);
498
499 /* Get the RR type and upper case it, and check that we recognize it. */
500
501 Ustrncpy(qtype, argv[3], sizeof(qtype));
502 qtypelen = Ustrlen(qtype);
503 for (p = qtype; *p != 0; p++) *p = toupper(*p);
504
505 /* Find the domain, lower case it, check that it is in a zone that we handle,
506 and set up the zone file name. The zone names in the table all start with a
507 dot. */
508
509 domlen = Ustrlen(argv[2]);
510 if (argv[2][domlen-1] == '.') domlen--;
511 Ustrncpy(domain, argv[2], domlen);
512 domain[domlen] = 0;
513 for (i = 0; i < domlen; i++) domain[i] = tolower(domain[i]);
514
515 if (Ustrchr(domain, '.') == NULL && qualify != NULL &&
516     Ustrcmp(domain, "dontqualify") != 0)
517   {
518   Ustrcat(domain, qualify);
519   domlen += Ustrlen(qualify);
520   }
521
522 for (i = 0; i < zonecount; i++)
523   {
524   int zlen;
525   zone = zones[i].zone;
526   zlen = Ustrlen(zone);
527   if (Ustrcmp(domain, zone+1) == 0 || (domlen >= zlen &&
528       Ustrcmp(domain + domlen - zlen, zone) == 0))
529     {
530     zonefile = zones[i].zonefile;
531     break;
532     }
533   }
534
535 if (zonefile == NULL)
536   {
537   fprintf(stderr, "fakens: query not in faked zone: domain is: %s\n", domain);
538   return PASS_ON;
539   }
540
541 (void)sprintf(buffer, "%s/../dnszones/%s", argv[1], zonefile);
542
543 /* Initialize the start of the response packet. We don't have to fake up
544 everything, because we know that Exim will look only at the answer and
545 additional section parts. */
546
547 memset(packet, 0, 12);
548 pk += 12;
549
550 /* Open the zone file. */
551
552 f = fopen(buffer, "r");
553 if (f == NULL)
554   {
555   fprintf(stderr, "fakens: failed to open %s: %s\n", buffer, strerror(errno));
556   return NO_RECOVERY;
557   }
558
559 /* Find the records we want, and add them to the result. */
560
561 count = 0;
562 yield = find_records(f, zone, domain, qtype, qtypelen, &pk, &count);
563 if (yield == NO_RECOVERY) goto END_OFF;
564
565 packet[6] = (count >> 8) & 255;
566 packet[7] = count & 255;
567
568 /* There is no need to return any additional records because Exim no longer
569 (from release 4.61) makes any use of them. */
570
571 packet[10] = 0;
572 packet[11] = 0;
573
574 /* Close the zone file, write the result, and return. */
575
576 END_OFF:
577 (void)fclose(f);
578 (void)fwrite(packet, 1, pk - packet, stdout);
579 return yield;
580 }
581
582 /* End of fakens.c */