diff --git a/Makefile.am b/Makefile.am index d12c3ce..a30d306 100644 --- a/Makefile.am +++ b/Makefile.am @@ -144,6 +144,7 @@ include_subdirs := \ src/apps/queuetool \ src/apps/storetool \ src/apps/storetool/demo \ + src/apps/testtool \ src/www \ zoneinfo \ man \ @@ -212,6 +213,7 @@ include src/apps/mdbtool/Bongo.rules include src/apps/queuetool/Bongo.rules include src/apps/storetool/Bongo.rules include src/apps/storetool/demo/Bongo.rules +include src/apps/testtool/Bongo.rules include src/www/Bongo.rules include zoneinfo/Bongo.rules include man/Bongo.rules diff --git a/configure.ac b/configure.ac index 811d791..435e565 100644 --- a/configure.ac +++ b/configure.ac @@ -670,6 +670,7 @@ src/apps/mdbtool/Makefile src/apps/queuetool/Makefile src/apps/storetool/Makefile src/apps/storetool/demo/Makefile +src/apps/testtool/Makefile src/libs/cal/Makefile src/libs/cal/tests/Makefile src/libs/calcmd/Makefile diff --git a/include/xpldns.h b/include/xpldns.h new file mode 100644 index 0000000..bd78abc --- /dev/null +++ b/include/xpldns.h @@ -0,0 +1,108 @@ +// + +#ifndef XPLDNS_H +#define XPLDNS_H + +#include + +#define XPLDNS_NAMELEN 256 + +typedef enum { + XPLDNS_INTERNAL_ERROR = -1, // internal error, see errno + XPLDNS_SUCCESS = 0, + XPLDNS_NOT_FOUND = 1, // authoritative host not found + XPLDNS_TRY_AGAIN = 2, // host not found or server failure + XPLDNS_DNS_ERROR = 3, // query refused/other non-recoverable + XPLDNS_NO_DATA = 4, // name valid, no records exist + XPLDNS_FAIL = 100, + XPLDNS_FAIL_CNAME_LOOP = 101 +} XplDns_ResultCode; + +typedef enum { + XPLDNS_RR_A = 0x0001, + XPLDNS_RR_CNAME = 0x0005, + XPLDNS_RR_SOA = 0x0006, + XPLDNS_RR_PTR = 0x000C, + XPLDNS_RR_MX = 0x000F, + XPLDNS_RR_TXT = 0x0010 +} XplDns_RecordType; + +typedef struct { + char name[XPLDNS_NAMELEN + 1]; // Domain name on MX record + char mxname[XPLDNS_NAMELEN + 1]; // address of record + unsigned short preference; // Preference of record +} XplDns_MxRecord; + +typedef struct { + char name[XPLDNS_NAMELEN + 1]; // Domain name on A record + int address; // IP address of record +} XplDns_ARecord; + +typedef struct { + char name[XPLDNS_NAMELEN + 1]; // Domain name on PTR record +} XplDns_PtrRecord; + +typedef struct { + char name[XPLDNS_NAMELEN + 1]; // Domain name on CNAME + char cname[XPLDNS_NAMELEN + 1]; // what it points to +} XplDns_CnameRecord; + +typedef struct { + char mname[XPLDNS_NAMELEN + 1]; // source host + char rname[XPLDNS_NAMELEN + 1]; // contact e-mail + unsigned long serial; // serial number + unsigned long refresh; // refresh time (secs) + unsigned long retry; // retry time (secs) + unsigned long expire; // expiry time (secs) + unsigned long minimum; // minimum TTL (secs) +} XplDns_SoaRecord; + +typedef struct { + char txt[XPLDNS_NAMELEN + 1]; // Text attached to record +} XplDns_TxtRecord; + +typedef union { + XplDns_ARecord A; + XplDns_CnameRecord CNAME; + XplDns_MxRecord MX; + XplDns_PtrRecord PTR; + XplDns_SoaRecord SOA; + XplDns_TxtRecord TXT; +} XplDns_Record; + +typedef struct _XplDns_RecordList XplDns_RecordList; + +struct _XplDns_RecordList { + XplDns_RecordList *next; + XplDns_RecordType type; + XplDns_Record record; +}; + +typedef struct { + XplDns_ResultCode status; + XplDns_RecordList *list; +} XplDns_Result; + +#define XPLDNS_MAX_IP_IN_LIST 20 +typedef struct{ + XplDns_ResultCode status; + int ip_list[XPLDNS_MAX_IP_IN_LIST + 1]; + int number; +} XplDns_IpList; + +typedef struct { + XplDns_ResultCode status; + int preference; + char current_mx[XPLDNS_NAMELEN + 1]; + XplDns_Result *_mx_result; + XplDns_RecordList *_mx_current; +} XplDns_MxLookup; + +int XplDnsInit(void); +void XplDnsResultFree(XplDns_Result *result); +XplDns_Result * XplDnsLookupIP(const char *domain); +XplDns_MxLookup *XplDnsNewMxLookup(const char *domain); +XplDns_IpList * XplDnsNextMxLookupIpList(XplDns_MxLookup *mx); +void XplDnsFreeMxLookup(XplDns_MxLookup *mx); + +#endif diff --git a/src/apps/testtool/Bongo.rules b/src/apps/testtool/Bongo.rules new file mode 100644 index 0000000..ae2750b --- /dev/null +++ b/src/apps/testtool/Bongo.rules @@ -0,0 +1,26 @@ +# -*- Makefile -*- +sbin_PROGRAMS += bongo-testtool + +bongo_testtool_SOURCES := \ + $(conf_defaults) \ + src/apps/testtool/config.h \ + src/apps/testtool/testtool.c + +bongo_testtool_CPPFLAGS := $(AM_CPPFLAGS) \ + $(GNUTLS_CFLAGS) + +bongo_testtool_LDADD := \ + libbongonmap.la \ + libbongomsgapi.la \ + libbongoconnio.la \ + libbongomemmgr.la \ + libbongoutil.la \ + libbongoxpl.la \ + $(PTHREAD_LIBS) \ + $(GNUTLS_LIBS) \ + $(ALL_LIBS) + +src/apps/testtool/all: bongo-testtool +src/apps/testtool/clean: clean +src/apps/testtool/install: + @cd $(top_builddir) && $(MAKE) $(MFLAGS) sbin_PROGRAMS=bongo-testtool install-sbinPROGRAMS diff --git a/src/apps/testtool/Makefile.am b/src/apps/testtool/Makefile.am new file mode 100644 index 0000000..8830401 --- /dev/null +++ b/src/apps/testtool/Makefile.am @@ -0,0 +1,17 @@ +# -*- Makefile -*- +# +# Do not edit! +# +# (unless this is Makefile.am.subdir) +# +# This file is a copy of the toplevel Makefile.am.subdir. If changes +# need to be made, edit that file, and make update-makefiles, and +# check the new files in. +# +# This is a skeletal automake file. The real automake fules for +# building the targets for this directory can be found in the +# Bongo.rules file. This file is simply here so that 'make all' can +# work in each subdirectory. + +all clean install: + @cd $(top_builddir) && $(MAKE) $(MFLAGS) $(subdir)/$@ diff --git a/src/apps/testtool/config.h b/src/apps/testtool/config.h new file mode 100644 index 0000000..6031769 --- /dev/null +++ b/src/apps/testtool/config.h @@ -0,0 +1,4 @@ +// private header + +void usage(void); +void LookupMxRecords(const char *domain); diff --git a/src/apps/testtool/testtool.c b/src/apps/testtool/testtool.c new file mode 100644 index 0000000..cea7f7d --- /dev/null +++ b/src/apps/testtool/testtool.c @@ -0,0 +1,112 @@ +/* This program is free software, licensed under the terms of the GNU GPL. + * See the Bongo COPYING file for full details + * Copyright (c) 2007 Alex Hudson + */ + +#include +#include +#include +#include +#include +#include "config.h" + +#include +#define _(x) gettext(x) + +void +usage(void) { + char *text = + "bongo-testtool - Test parts of the Bongo system.\n\n" + "Usage: bongo-testtool [command]\n\n" + "Commands:\n" + " checkmx Search for mail exchangers\n" + ""; + + XplConsolePrintf("%s", text); +} + +void +LookupMxRecords(const char *domain) +{ + XplDns_MxLookup *mx; + XplDns_IpList *list; + + mx = XplDnsNewMxLookup(domain); + if (!mx || mx->status != XPLDNS_SUCCESS) { + XplConsolePrintf(_("ERROR: Unable to resolve MX records on %s"), domain); + return; + } + + // FIXME. We shouldn't try to relay to MXes who are lower preference + // than we are, if we're in the list (to prevent mail loops). + // In practice, this means stopping as soon as come to an IP we + // recognise as being ours. + while((list = XplDnsNextMxLookupIpList(mx)) != NULL) { + int x; + + XplConsolePrintf(_("%s (preference: %d):\n"), + mx->current_mx, mx->preference); + for (x = 0; x < list->number; x++) { + struct sockaddr_in addr; + addr.sin_addr.s_addr = list->ip_list[x]; + XplConsolePrintf(" - %s\n", inet_ntoa(addr.sin_addr)); + } + MemFree(list); + } + + XplDnsFreeMxLookup(mx); +} + +int +main(int argc, char *argv[]) { + int next_arg = 0; + int command = 0; + int startup = 0; + + if (!MemoryManagerOpen("bongo-testtool")) { + XplConsolePrintf(_("ERROR: Failed to initialize memory manager\n")); + return 1; + } + + // parse options + while (++next_arg < argc && argv[next_arg][0] == '-') { + char *arg = argv[next_arg]; + printf(_("Unrecognized option: %s\n"), argv[next_arg]); + } + + // parse command + if (next_arg < argc) { + if (!strcmp(argv[next_arg], "checkmx")) { + command = 1; + } else { + printf(_("Unrecognized command: %s\n"), argv[next_arg]); + } + } else { + printf(_("ERROR: No command specified\n")); + usage(); + return 1; + } + + XplInit(); + + if (XplSetRealUser(MsgGetUnprivilegedUser()) < 0) { + printf(_("bongo-testtool: Could not drop to unprivileged user '%s'\n"), MsgGetUnprivilegedUser()); + exit(-1); + } + + switch(command) { + case 1: + next_arg++; + if (next_arg >= argc) { + printf(_("Usage: checkmx \n")); + } else { + LookupMxRecords(argv[next_arg]); + } + break; + default: + break; + } + + MemoryManagerClose("bongo-testtool"); + exit(0); +} diff --git a/src/libs/xpl/dns.c b/src/libs/xpl/dns.c index 357553f..8fa94bb 100644 --- a/src/libs/xpl/dns.c +++ b/src/libs/xpl/dns.c @@ -1,10 +1,470 @@ // Xpl DNS resolution routines. #include +#include +#include +#include +#include + +#include "dns.h" #include "config.h" -BOOL +int XplDnsInit() { - return FALSE; + // res_init() may set errno, so we clear it and then test later + errno = 0; + (void) res_init(); + + if (errno != 0) + return -1; + return 0; +} + +int +_XplDns_ParseName(const char *response, const int response_len, const char *name, char *buffer, int buffer_len) +{ + const char *ptr; + int len = 0; + int result = 0; + + if (buffer_len < 2) return -1; // out of space + len = buffer_len - strlen(buffer); + if (len <= 0) return -1; // out of space + + ptr = name; + while ((response <= ptr) && (ptr < (response + response_len))) { + if (*ptr == '\0') { + // we've reached the end + ptr++; + break; + } + if ((*ptr & 0xc0) == 0xc0) { + int result; + int newptr; + // this is a pointer to a previous label + newptr = (*ptr++ & 0x3f) * 256; + newptr += *ptr++; + if (newptr == (ptr - response - 2)) + return -4; // cyclical pointer; bad. + result = _XplDns_ParseName(response, response_len, (response + newptr), buffer, buffer_len); + if (result <= 0) return result; + break; + } else { + // append the next label if we have room in the buffer + if ((*ptr + 1) < len) { + strncat(buffer, ptr + 1, *ptr); + strncat(buffer, ".", 1); + len -= *ptr + 1; + ptr += *ptr + 1; + } else { + // out of room! + return -1; + } + } + } + + result = ptr - name; + if ((result < 0) || (result > response_len)) + // a big mistake? + return -2; + + return result; +} + +#define IS_MX(r) ((r)->type == XPLDNS_RR_MX) + +void +_XplDnsResult_AppendRecord(XplDns_Result *result, XplDns_RecordList *item) +{ + // this doesn't always append: if the item is an MX record, it will + // be inserted before MX records of higher preference, or appended. + // Thus, the record list always has MX records in order. + // TOOD: Equal preference MX records are sorted in reply order (e.g., + // round-robin). We're supposed to randomise them. + XplDns_RecordList *end; + + item->next = NULL; + + if (result->list == NULL) { + // no items in list + result->list = item; + } else { //if (result->list->next == NULL) { + // one item already in list + end = result->list; + // if this is an MX RR, see if we can insert before the first + // item in our list + if (IS_MX(item) && IS_MX(end)) { + // TODO: if pref==pref, then we should randomise + // See RFC 2821 #5 + if (end->record.MX.preference >= item->record.MX.preference) { + item->next = result->list; + result->list = item; + return; + } + } + + while (end->next != NULL) { + // see if we can insert before a later MX record + if (IS_MX(item) && IS_MX(end->next)) { + // See above TODO re: randomisation. + if (end->next->record.MX.preference >= + item->record.MX.preference) { + // insert here! + item->next = end->next; + end->next = item; + return; + } + } + end = end->next; + } + end->next = item; + } +} + +void +_XplDnsResult_PrintResults(XplDns_Result *result) +{ + XplDns_RecordList *item; + + if (result == NULL) return; + item = result->list; + + printf ("\n== START OF DNS RECORD LIST\n"); + while (item != NULL) { + struct sockaddr_in addr; + + switch (item->type) { + case XPLDNS_RR_A: + addr.sin_addr.s_addr = item->record.A.address; + printf("%s IN A %s\n", item->record.A.name, inet_ntoa(addr.sin_addr)); + break; + case XPLDNS_RR_CNAME: + printf("%s IN CNAME %s\n", item->record.CNAME.name, item->record.CNAME.cname); + break; + case XPLDNS_RR_MX: + printf("%s IN MX %d %s\n", + item->record.MX.name, + item->record.MX.preference, + item->record.MX.mxname); + break; + default: + printf("Unknown record.\n"); + break; + } + item = item->next; + } + printf ("== END OF RESULTS\n"); +} + +void +XplDnsResultFree(XplDns_Result *result) +{ + XplDns_RecordList *item = result->list; + XplDns_RecordList *last; + + if (result == NULL) return; + + while (item != NULL) { + last = item; + item = item->next; + MemFree(last); + } + MemFree(result); +} + +XplDns_MxLookup * +XplDnsNewMxLookup(const char *domain) +{ + // Lazy MX resolver. + // Should comply with RFC 2821 #5 + XplDns_MxLookup *result; + XplDns_Result *mx_query; + char mail_domain[XPLDNS_NAMELEN + 1]; + int cnames_found = 0; + + result = MemMalloc0(sizeof(XplDns_MxLookup)); + if (! result) return NULL; + + strncpy(mail_domain, domain, XPLDNS_NAMELEN); + + // we come back to this point if we encounter CNAMEs before MX RRs +restart_query: + mx_query = _XplDns_Query(mail_domain, XPLDNS_RR_MX); + + if (mx_query->status == XPLDNS_NO_DATA) { + // domain exists, but no MX records + XplDns_Result *a_query; + + a_query = _XplDns_Query(domain, XPLDNS_RR_A); + if (a_query->status == XPLDNS_SUCCESS) { + // A RR exists, so let's fake an MX record for it + XplDnsResultFree(mx_query); + XplDnsResultFree(a_query); + mx_query = _XplDns_QueryFakeMx(domain); + } + } else if (mx_query->status == XPLDNS_SUCCESS) { + // check we don't have any CNAME in results + XplDns_RecordList *record = mx_query->list; + + while (record) { + if (record->type == XPLDNS_RR_CNAME) { + // restart our query on new domain + strncpy(mail_domain, record->record.CNAME.cname, + XPLDNS_NAMELEN); + XplDnsResultFree(mx_query); + + cnames_found++; + if (cnames_found < 6) { + goto restart_query; + } else { + result->status = XPLDNS_FAIL_CNAME_LOOP; + return result; + } + } else { + record = record->next; + } + } + } + + result->status = mx_query->status; + result->_mx_result = mx_query; + result->_mx_current = mx_query->list; + + return result; +} + +XplDns_IpList * +XplDnsNextMxLookupIpList(XplDns_MxLookup *mx) +{ + XplDns_IpList *result; + XplDns_Result *lookup; + XplDns_RecordList *record; + const char *mail_exchanger = NULL; + + if (mx->_mx_current == NULL) return NULL; + result = MemMalloc(sizeof(XplDns_IpList)); + if (! result) return NULL; + + result->status = XPLDNS_TRY_AGAIN; + result->number = 0; + + if (mx->_mx_current->type == XPLDNS_RR_MX) + // want to make sure we're looking at the right records. + mail_exchanger = mx->_mx_current->record.MX.mxname; + + // no decent records at this point? + if (! mail_exchanger) return result; + + strncpy(mx->current_mx, mail_exchanger, XPLDNS_NAMELEN); + mx->preference = mx->_mx_current->record.MX.preference; + mx->_mx_current = mx->_mx_current->next; + + // look up the mail server's IP address(es) + lookup = _XplDns_Query(mail_exchanger, XPLDNS_RR_A); + + if (lookup->status != XPLDNS_SUCCESS) { + // more failure... :/ + result->status = lookup->status; + return result; + } + + record = lookup->list; + while (record) { + // FIXME: check for CNAME entries? + if (record->type != XPLDNS_RR_A) { + record = record->next; + continue; // not interested in non-A records + } + + result->ip_list[result->number] = record->record.A.address; + result->number++; + if (result->number >= XPLDNS_MAX_IP_IN_LIST) + break; + record = record->next; + } + + XplDnsResultFree(lookup); + return result; +} + +void +XplDnsFreeMxLookup(XplDns_MxLookup *mx) +{ + if (mx == NULL) return; + + if (mx->_mx_result) { + XplDnsResultFree(mx->_mx_result); + mx->_mx_result = NULL; + } + + MemFree(mx); +} + +XplDns_Result * +_XplDns_QueryFakeMx(const char *domain) +{ + // when a domain has no MX RR, RFC 2821 says treat any A record + // for that domain as having an implicit MX record. This creates + // that record, as if it existed in DNS. + XplDns_Result *result; + XplDns_RecordList *list; + + list = MemMalloc(sizeof(XplDns_RecordList)); + + strncpy(list->record.MX.name, domain, XPLDNS_NAMELEN); + strncpy(list->record.MX.mxname, domain, XPLDNS_NAMELEN); + list->record.MX.preference = 0; + list->type = XPLDNS_RR_MX; + + result = MemMalloc(sizeof(XplDns_Result)); + result->list = list; + result->status = XPLDNS_SUCCESS; + + return result; +} + +XplDns_Result * +_XplDns_Query(const char *domain, XplDns_RecordType type) +{ + XplDns_Result *result; + char answer_buffer[4096]; + int answer_len = -1; + + result = MemMalloc(sizeof(XplDns_Result)); + result->list = NULL; + result->status = XPLDNS_FAIL; + + // FIXME: is this re-entrant _everywhere_ ? + answer_len = res_query(domain, ns_c_any, type, answer_buffer, 4096); + + if (answer_len < 0) { + // didn't get a sensible answer + if (h_errno) { + result->status = (XplDns_ResultCode) h_errno; + return result; + } else { + return result; + } + } + + if (answer_len == 0) { + // no records in the answer + result->status = XPLDNS_NO_DATA; + return result; + } + + _XplDns_ParseQuery(answer_buffer, answer_len, result); + + return result; +} + +void +_XplDns_ParseQuery(const char *answer_buffer, int answer_len, XplDns_Result *result) +{ + XplDnsResponseHeader *header; + const char *response, *response_end; + int x; + int res; + int answers; + + // try to parse the answer + header = (XplDnsResponseHeader *)answer_buffer; + response = answer_buffer + sizeof(XplDnsResponseHeader); + response_end = answer_buffer + answer_len; + + for (x = 0; x < ntohs(header->qdcount); x++) { + int size; + char buffer[1024]; + + buffer[0] = '\0'; + size = _XplDns_ParseName(answer_buffer, answer_len, response, buffer, 1000); + // did we find the name correctly? + if (size < 0) return; + + // Skip QCODE / QCLASS + response += size + 4; + + // does the buffer extent look right? + if (response >= response_end) return; + } + + for (x = 0; x < ntohs(header->ancount); x++) { + XplDnsAnswer *dns_answer; + XplDns_RecordList *item; + char answer_domain[XPLDNS_NAMELEN + 1]; + + item = MemMalloc(sizeof(XplDns_RecordList)); + + answer_domain[0] = '\0'; + res = _XplDns_ParseName(answer_buffer, answer_len, + response, answer_domain, XPLDNS_NAMELEN); + if (res < 0) return; // couldn't parse name... + + response += res; + if (response >= response_end) return; + + dns_answer = (XplDnsAnswer *)response; + + response += sizeof(XplDnsAnswer); + if (response >= response_end) return; + + // TODO : check ntohs(ans->class) is right + + switch(ntohs(dns_answer->rtype)) { + case XPLDNS_RR_A: { + item->type = XPLDNS_RR_A; + XplDnsAnswerARecord *rec = (XplDnsAnswerARecord *)response; + strncpy(item->record.A.name, answer_domain, XPLDNS_NAMELEN); + item->record.A.address = rec->address; + _XplDnsResult_AppendRecord(result, item); + } + break; + case XPLDNS_RR_CNAME: { + item->type = XPLDNS_RR_CNAME; + + strncpy(item->record.CNAME.name, answer_domain, + XPLDNS_NAMELEN); + + item->record.CNAME.cname[0] = '\0'; + _XplDns_ParseName(answer_buffer, answer_len, response, item->record.CNAME.cname, XPLDNS_NAMELEN); + _XplDnsResult_AppendRecord(result, item); + + } + break; + case XPLDNS_RR_MX: { + XplDnsAnswerMxRecord *rec = (XplDnsAnswerMxRecord *)response; + int r; + + strncpy(item->record.MX.name, answer_domain, + XPLDNS_NAMELEN); + item->type = XPLDNS_RR_MX; + item->record.MX.preference = ntohs(rec->pref); + + item->record.MX.mxname[0] = '\0'; + r = _XplDns_ParseName(answer_buffer, + answer_len, + response + sizeof(XplDnsAnswerMxRecord), + item->record.MX.mxname, + XPLDNS_NAMELEN); + if (r >= 0) { + _XplDnsResult_AppendRecord(result, item); + } else { + return; + } + } + break; + default: + // unknown type. + MemFree(item); + break; + } + + response += ntohs(dns_answer->size); + if (response >= response_end) return; + } + + // don't make the result 'success' until we've really done everything + result->status = XPLDNS_SUCCESS; + return; } diff --git a/src/libs/xpl/tests/dns-test.c b/src/libs/xpl/tests/dns-test.c index 0428090..42a0cf9 100644 --- a/src/libs/xpl/tests/dns-test.c +++ b/src/libs/xpl/tests/dns-test.c @@ -1,17 +1,35 @@ // Test the Xpl DNS resolution code #include +#include +#include #include #ifdef BONGO_HAVE_CHECK +#define MEM_NAME "check-xpl-dns" + START_TEST(test1) { - fail_unless(1==1); + XplDns_MxLookup *mx; + XplDns_IpList *list; + + MemoryManagerOpen(MEM_NAME); + + fail_unless(XplDnsInit() == 0); + + mx = XplDnsNewMxLookup("bongo-project.org"); + + fail_unless(mx != NULL); + fail_unless(mx->status == XPLDNS_SUCCESS); + + XplDnsFreeMxLookup(mx); + + MemoryManagerClose(MEM_NAME); } END_TEST -START_CHECK_SUITE_SETUP("Testing the Xpl DNS routines") +START_CHECK_SUITE_SETUP("Xpl DNS routine tests") CREATE_CHECK_CASE (tc_core , "Core" ); CHECK_SUITE_ADD_CASE(top_suite, tc_core ); CHECK_CASE_ADD_TEST (tc_core , test1 );