Made listener_print be CONST
[freeradius.git] / src / main / detail.c
index b68487c..5fdb575 100644 (file)
@@ -33,36 +33,28 @@ RCSID("$Id$")
 #include <sys/stat.h>
 #endif
 
+#ifdef HAVE_GLOB_H
+#include <glob.h>
+#endif
+
 #include <fcntl.h>
 
+#ifdef WITH_DETAIL
+
 #define USEC (1000000)
 
-typedef struct listen_detail_t {
-       int             delay_time; /* should be first entry */
-       char    *filename;
-       VALUE_PAIR      *vps;
-       FILE            *fp;
-       int             state;
-       time_t          timestamp;
-       fr_ipaddr_t     client_ip;
-       int             load_factor; /* 1..100 */
-
-       int             has_rtt;
-       int             srtt;
-       int             rttvar;
-       struct timeval  last_packet;
-       RADCLIENT       detail_client;
-} listen_detail_t;
-
-
-#define STATE_UNOPENED (0)
-#define STATE_UNLOCKED (1)
-#define STATE_HEADER   (2)
-#define STATE_READING  (3)
-#define STATE_QUEUED   (4)
-#define STATE_RUNNING  (5)
-#define STATE_NO_REPLY (6)
-#define STATE_REPLIED  (7)
+static FR_NAME_NUMBER state_names[] = {
+       { "unopened", STATE_UNOPENED },
+       { "unlocked", STATE_UNLOCKED },
+       { "header", STATE_HEADER },
+       { "reading", STATE_READING },
+       { "queued", STATE_QUEUED },
+       { "running", STATE_RUNNING },
+       { "no-reply", STATE_NO_REPLY },
+       { "replied", STATE_REPLIED },
+
+       { NULL, 0 }
+};
 
 /*
  *     If we're limiting outstanding packets, then mark the response
@@ -82,13 +74,19 @@ int detail_send(rad_listen_t *listener, REQUEST *request)
         *      caller it's OK to read more "detail" file stuff.
         */
        if (request->reply->code == 0) {
-               radius_signal_self(RADIUS_SIGNAL_SELF_DETAIL);
+               data->delay_time = data->retry_interval * USEC;
+               data->signal = 1;
                data->state = STATE_NO_REPLY;
+
+               RDEBUG("Detail - No response configured for request %d.  Will retry in %d seconds",
+                      request->number, data->retry_interval);
+
+               radius_signal_self(RADIUS_SIGNAL_SELF_DETAIL);
                return 0;
        }
 
        /*
-        *      We call gettimeofday a lot.  But here it should be OK,
+        *      We call gettimeofday a lot.  But it should be OK,
         *      because there's nothing else to do.
         */
        gettimeofday(&now, NULL);
@@ -147,10 +145,19 @@ int detail_send(rad_listen_t *listener, REQUEST *request)
         */
        data->delay_time = (data->srtt * (100 - data->load_factor)) / (data->load_factor);
 
-       DEBUG2("RTT %d\tdelay %d", data->srtt, data->delay_time);
-
+       /*
+        *      Cap delay at 4 packets/s.  If the end system can't
+        *      handle this, then it's very broken.
+        */
+       if (data->delay_time > (USEC / 4)) data->delay_time= USEC / 4;
+       
+       RDEBUG3("Received response for request %d.  Will read the next packet in %d seconds",
+               request->number, data->delay_time / USEC);
+       
        data->last_packet = now;
+       data->signal = 1;
        data->state = STATE_REPLIED;
+       radius_signal_self(RADIUS_SIGNAL_SELF_DETAIL);
 
        return 0;
 }
@@ -165,11 +172,11 @@ int detail_send(rad_listen_t *listener, REQUEST *request)
 static int detail_open(rad_listen_t *this)
 {
        struct stat st;
-       char buffer[2048];
        listen_detail_t *data = this->data;
+       char *filename = data->filename;
 
        rad_assert(data->state == STATE_UNOPENED);
-       snprintf(buffer, sizeof(buffer), "%s.work", data->filename);
+       data->delay_time = USEC;
 
        /*
         *      Open detail.work first, so we don't lose
@@ -180,9 +187,15 @@ static int detail_open(rad_listen_t *this)
         *      we've got to open it for writing in order to
         *      establish the lock, to prevent rlm_detail from
         *      writing to it.
+        *
+        *      This also means that if we're doing globbing,
+        *      this file will be read && processed before the
+        *      file globbing is done.
         */
-       this->fd = open(buffer, O_RDWR);
+       this->fd = open(data->filename_work, O_RDWR);
        if (this->fd < 0) {
+               DEBUG2("Polling for detail file %s", filename);
+
                /*
                 *      Try reading the detail file.  If it
                 *      doesn't exist, we can't do anything.
@@ -191,45 +204,77 @@ static int detail_open(rad_listen_t *this)
                 *      exists, even if we don't have permissions
                 *      to read it.
                 */
-               if (stat(data->filename, &st) < 0) {
+               if (stat(filename, &st) < 0) {
+#ifdef HAVE_GLOB_H
+                       unsigned int i;
+                       int found;
+                       time_t chtime;
+                       glob_t files;
+
+                       memset(&files, 0, sizeof(files));
+                       if (glob(filename, 0, NULL, &files) != 0) {
+                               return 0;
+                       }
+
+                       chtime = 0;
+                       found = -1;
+                       for (i = 0; i < files.gl_pathc; i++) {
+                               if (stat(files.gl_pathv[i], &st) < 0) continue;
+
+                               if ((i == 0) ||
+                                   (st.st_ctime < chtime)) {
+                                       chtime = st.st_ctime;
+                                       found = i;
+                               }
+                       }
+
+                       if (found < 0) {
+                               globfree(&files);
+                               return 0;
+                       }
+
+                       filename = strdup(files.gl_pathv[found]);
+                       globfree(&files);
+#else
                        return 0;
+#endif
                }
 
                /*
                 *      Open it BEFORE we rename it, just to
                 *      be safe...
                 */
-               this->fd = open(data->filename, O_RDWR);
+               this->fd = open(filename, O_RDWR);
                if (this->fd < 0) {
-                       radlog(L_ERR, "Failed to open %s: %s",
-                              data->filename, strerror(errno));
+                       radlog(L_ERR, "Detail - Failed to open %s: %s",
+                              filename, strerror(errno));
+                       if (filename != data->filename) free(filename);
                        return 0;
                }
 
                /*
                 *      Rename detail to detail.work
                 */
-               if (rename(data->filename, buffer) < 0) {
+               DEBUG("Detail - Renaming %s -> %s", filename, data->filename_work);
+               if (rename(filename, data->filename_work) < 0) {
+                       if (filename != data->filename) free(filename);
                        close(this->fd);
                        this->fd = -1;
                        return 0;
                }
+               if (filename != data->filename) free(filename);
        } /* else detail.work existed, and we opened it */
 
        rad_assert(data->vps == NULL);
-
        rad_assert(data->fp == NULL);
-       data->fp = fdopen(this->fd, "r");
-       if (!data->fp) {
-               radlog(L_ERR, "Failed to re-open %s: %s",
-                      data->filename, strerror(errno));
-               return 0;
-       }
 
        data->state = STATE_UNLOCKED;
 
        data->client_ip.af = AF_UNSPEC;
        data->timestamp = 0;
+       data->offset = 0;
+       data->packets = 0;
+       data->tries = 0;
 
        return 1;
 }
@@ -253,21 +298,23 @@ static int detail_open(rad_listen_t *this)
 int detail_recv(rad_listen_t *listener,
                RAD_REQUEST_FUNP *pfun, REQUEST **prequest)
 {
-       char            key[256], value[1024];
+       char            key[256], op[8], value[1024];
        VALUE_PAIR      *vp, **tail;
        RADIUS_PACKET   *packet;
        char            buffer[2048];
        listen_detail_t *data = listener->data;
 
+       /*
+        *      We may be in the main thread.  It needs to update the
+        *      timers before we try to read from the file again.
+        */
+       if (data->signal) return 0;
+
        switch (data->state) {
                case STATE_UNOPENED:
+       open_file:
                        rad_assert(listener->fd < 0);
                        
-                       /*
-                        *      FIXME: If the file doesn't exist, then
-                        *      return "sleep for 1s", to avoid busy
-                        *      looping.
-                        */
                        if (!detail_open(listener)) return 0;
 
                        rad_assert(data->state == STATE_UNLOCKED);
@@ -294,28 +341,60 @@ int detail_recv(rad_listen_t *listener,
                         *      radrelay.
                         */
                        if (rad_lockfd_nonblock(listener->fd, 0) < 0) {
+                               /*
+                                *      Close the FD.  The main loop
+                                *      will wake up in a second and
+                                *      try again.
+                                */
+                               close(listener->fd);
+                               listener->fd = -1;
+                               data->state = STATE_UNOPENED;
                                return 0;
                        }
+
+                       data->fp = fdopen(listener->fd, "r");
+                       if (!data->fp) {
+                               radlog(L_ERR, "FATAL: Failed to re-open detail file %s: %s",
+                                      data->filename, strerror(errno));
+                               exit(1);
+                       }
+
                        /*
                         *      Look for the header
                         */
                        data->state = STATE_HEADER;
+                       data->delay_time = USEC;
                        data->vps = NULL;
 
                        /* FALL-THROUGH */
 
                case STATE_HEADER:
                do_header:
+                       data->tries = 0;
+                       if (!data->fp) {
+                               data->state = STATE_UNOPENED;
+                               goto open_file;
+                       }
+
+                       {
+                               struct stat buf;
+                               
+                               fstat(listener->fd, &buf);
+                               if (((off_t) ftell(data->fp)) == buf.st_size) {
+                                       goto cleanup;
+                               }
+                       }
+
                        /*
                         *      End of file.  Delete it, and re-set
                         *      everything.
                         */
                        if (feof(data->fp)) {
                        cleanup:
-                               snprintf(buffer, sizeof(buffer),
-                                        "%s.work", data->filename);
-                               unlink(buffer);
-                               fclose(data->fp); /* closes listener->fd */
+                               DEBUG("Detail - unlinking %s",
+                                     data->filename_work);
+                               unlink(data->filename_work);
+                               if (data->fp) fclose(data->fp);
                                data->fp = NULL;
                                listener->fd = -1;
                                data->state = STATE_UNOPENED;
@@ -334,7 +413,7 @@ int detail_recv(rad_listen_t *listener,
                         *      we have.
                         */
                case STATE_READING:
-                       if (!feof(data->fp)) break;
+                       if (data->fp && !feof(data->fp)) break;
                        data->state = STATE_QUEUED;
 
                        /* FALL-THROUGH */
@@ -343,11 +422,18 @@ int detail_recv(rad_listen_t *listener,
                        goto alloc_packet;
 
                        /*
-                        *      We still have an outstanding packet.
-                        *      Don't read any more.
+                        *      Periodically check what's going on.
+                        *      If the request is taking too long,
+                        *      retry it.
                         */
                case STATE_RUNNING:
-                       return 0;
+                       if (time(NULL) < (data->running + data->retry_interval)) {
+                               return 0;
+                       }
+
+                       DEBUG("No response to detail request.  Retrying");
+                       data->state = STATE_NO_REPLY;
+                       /* FALL-THROUGH */
 
                        /*
                         *      If there's no reply, keep
@@ -375,6 +461,8 @@ int detail_recv(rad_listen_t *listener,
         *      Read a header, OR a value-pair.
         */
        while (fgets(buffer, sizeof(buffer), data->fp)) {
+               data->offset = ftell(data->fp); /* for statistics */
+
                /*
                 *      Badly formatted file: delete it.
                 *
@@ -415,11 +503,18 @@ int detail_recv(rad_listen_t *listener,
                 *
                 *      FIXME: print an error for badly formatted attributes?
                 */
-               if (sscanf(buffer, "%255s = %1023s", key, value) != 2) {
+               if (sscanf(buffer, "%255s %8s %1023s", key, op, value) != 3) {
+                       DEBUG2("WARNING: Skipping badly formatted line %s",
+                              buffer);
                        continue;
                }
 
                /*
+                *      Should be =, :=, +=, ...
+                */
+               if (!strchr(op, '=')) continue;
+
+               /*
                 *      Skip non-protocol attributes.
                 */
                if (!strcasecmp(key, "Request-Authenticator")) continue;
@@ -444,6 +539,14 @@ int detail_recv(rad_listen_t *listener,
                 */
                if (!strcasecmp(key, "Timestamp")) {
                        data->timestamp = atoi(value);
+
+                       vp = paircreate(PW_PACKET_ORIGINAL_TIMESTAMP,
+                                       PW_TYPE_DATE);
+                       if (vp) {
+                               vp->vp_date = (uint32_t) data->timestamp;
+                               *tail = vp;
+                               tail = &(vp->next);
+                       }
                        continue;
                }
 
@@ -469,11 +572,25 @@ int detail_recv(rad_listen_t *listener,
         */
        if (ferror(data->fp)) goto cleanup;
 
+       data->tries = 0;
+       data->packets++;
+
        /*
         *      Process the packet.
         */
  alloc_packet:
-       rad_assert(data->state == STATE_QUEUED);
+       data->tries++;
+       
+       /*
+        *      The writer doesn't check that the record was
+        *      completely written.  If the disk is full, this can
+        *      result in a truncated record.  When that happens,
+        *      treat it as EOF.
+        */
+       if (data->state != STATE_QUEUED) {
+               radlog(L_ERR, "Truncated record: treating it as EOF for detail file %s", data->filename_work);
+               goto cleanup;     
+       }
 
        /*
         *      We're done reading the file, but we didn't read
@@ -481,6 +598,7 @@ int detail_recv(rad_listen_t *listener,
         */
        if (!data->vps) {
                data->state = STATE_HEADER;
+               if (feof(data->fp)) goto cleanup; 
                return 0;
        }
 
@@ -490,8 +608,8 @@ int detail_recv(rad_listen_t *listener,
         */
        packet = rad_alloc(1);
        if (!packet) {
-               data->state = STATE_NO_REPLY;   /* try again later */
-               return 0;       /* maybe memory will magically free up... */
+               radlog(L_ERR, "FATAL: Failed allocating memory for detail");
+               exit(1);
        }
 
        memset(packet, 0, sizeof(*packet));
@@ -574,14 +692,23 @@ int detail_recv(rad_listen_t *listener,
                vp->vp_integer += time(NULL) - data->timestamp;
        }
 
+       /*
+        *      Set the transmission count.
+        */
+       vp = pairfind(packet->vps, PW_PACKET_TRANSMIT_COUNTER);
+       if (!vp) {
+               vp = paircreate(PW_PACKET_TRANSMIT_COUNTER, PW_TYPE_INTEGER);
+               rad_assert(vp != NULL);
+               pairadd(&packet->vps, vp);
+       }
+       vp->vp_integer = data->tries;
+
        *pfun = rad_accounting;
 
        if (debug_flag) {
-               printf("detail_recv: Read packet from %s\n", data->filename);
+               fr_printf_log("detail_recv: Read packet from %s\n", data->filename_work);
                for (vp = packet->vps; vp; vp = vp->next) {
-                       putchar('\t');
-                       vp_print(stdout, vp);
-                       putchar('\n');
+                       debug_pair(vp);
                }
        }
 
@@ -599,6 +726,7 @@ int detail_recv(rad_listen_t *listener,
        }
 
        data->state = STATE_RUNNING;
+       data->running = packet->timestamp;
 
        return 1;
 }
@@ -612,13 +740,17 @@ void detail_free(rad_listen_t *this)
        listen_detail_t *data = this->data;
 
        free(data->filename);
+       data->filename = NULL;
        pairfree(&data->vps);
 
-       if (data->fp != NULL) fclose(data->fp);
+       if (data->fp != NULL) {
+               fclose(data->fp);
+               data->fp = NULL;
+       }
 }
 
 
-int detail_print(rad_listen_t *this, char *buffer, size_t bufsize)
+int detail_print(const rad_listen_t *this, char *buffer, size_t bufsize)
 {
        if (!this->server) {
                return snprintf(buffer, bufsize, "%s",
@@ -630,20 +762,53 @@ int detail_print(rad_listen_t *this, char *buffer, size_t bufsize)
                        this->server);
 }
 
-int detail_encode(UNUSED rad_listen_t *this, UNUSED REQUEST *request)
+/*
+ *     Overloaded to return delay times.
+ */
+int detail_encode(rad_listen_t *this, UNUSED REQUEST *request)
 {
+       listen_detail_t *data = this->data;
+
        /*
-        *      We never encode responses "sent to" the detail file.
+        *      We haven't sent a packet... delay things a bit.
         */
-       return 0;
+       if (!data->signal) {
+               int delay = (data->poll_interval - 1) * USEC;
+
+               /*
+                *      Add +/- 0.25s of jitter
+                */
+               delay += (USEC * 3) / 4;
+               delay += fr_rand() % (USEC / 2);
+
+               DEBUG2("Detail listener %s state %s signalled %d waiting %d.%06d sec",
+                      data->filename,
+                      fr_int2str(state_names, data->state, "?"), data->signal,
+                      (delay / USEC), delay % USEC);
+
+               return delay;
+       }
+
+       data->signal = 0;
+       
+       DEBUG2("Detail listener %s state %s signalled %d waiting %d.%06d sec",
+              data->filename, fr_int2str(state_names, data->state, "?"),
+              data->signal,
+              data->delay_time / USEC,
+              data->delay_time % USEC);
+
+       return data->delay_time;
 }
 
-int detail_decode(UNUSED rad_listen_t *this, UNUSED REQUEST *request)
+
+/*
+ *     Overloaded to return "should we fix delay times"
+ */
+int detail_decode(rad_listen_t *this, UNUSED REQUEST *request)
 {
-       /*
-        *      We never decode responses read from the detail file.
-        */
-       return 0;
+       listen_detail_t *data = this->data;
+
+       return data->signal;
 }
 
 
@@ -652,10 +817,15 @@ static const CONF_PARSER detail_config[] = {
          offsetof(listen_detail_t, filename), NULL,  NULL },
        { "load_factor",   PW_TYPE_INTEGER,
          offsetof(listen_detail_t, load_factor), NULL, Stringify(10)},
+       { "poll_interval",   PW_TYPE_INTEGER,
+         offsetof(listen_detail_t, poll_interval), NULL, Stringify(1)},
+       { "retry_interval",   PW_TYPE_INTEGER,
+         offsetof(listen_detail_t, retry_interval), NULL, Stringify(30)},
 
        { NULL, -1, 0, NULL, NULL }             /* end the list */
 };
 
+extern int check_config;
 
 /*
  *     Parse a detail section.
@@ -665,10 +835,13 @@ int detail_parse(CONF_SECTION *cs, rad_listen_t *this)
        int             rcode;
        listen_detail_t *data;
        RADCLIENT       *client;
+       char buffer[2048];
+
+       if (check_config) return 0;
 
        if (!this->data) {
-               this->data = rad_malloc(sizeof(listen_detail_t));
-               memset(this->data, 0, sizeof(listen_detail_t));
+               this->data = rad_malloc(sizeof(*data));
+               memset(this->data, 0, sizeof(*data));
        }
 
        data = this->data;
@@ -689,20 +862,57 @@ int detail_parse(CONF_SECTION *cs, rad_listen_t *this)
                return -1;
        }
 
+       if ((data->poll_interval < 1) || (data->poll_interval > 20)) {
+               cf_log_err(cf_sectiontoitem(cs), "poll_interval must be between 1 and 20");
+               return -1;
+       }
+
+       /*
+        *      If the filename is a glob, use "detail.work" as the
+        *      work file name.
+        */
+       if ((strchr(data->filename, '*') != NULL) ||
+           (strchr(data->filename, '[') != NULL)) {
+               char *p;
+
+#ifndef HAVE_GLOB_H
+               radlog(L_INFO, "WARNING: Detail file \"%s\" appears to use file globbing, but it is not supported on this system.", data->filename);
+#endif
+               strlcpy(buffer, data->filename, sizeof(buffer));
+               p = strrchr(buffer, FR_DIR_SEP);
+               if (p) {
+                       p[1] = '\0';
+               } else {
+                       buffer[0] = '\0';
+               }
+               strlcat(buffer, "detail.work",
+                       sizeof(buffer) - strlen(buffer));
+                       
+       } else {
+               snprintf(buffer, sizeof(buffer), "%s.work", data->filename);
+       }
+
+       free(data->filename_work);
+       data->filename_work = strdup(buffer); /* FIXME: leaked */
+
        data->vps = NULL;
        data->fp = NULL;
        data->state = STATE_UNOPENED;
+       data->delay_time = data->poll_interval * USEC;
+       data->signal = 1;
 
        /*
         *      Initialize the fake client.
         */
        client = &data->detail_client;
+       memset(client, 0, sizeof(*client));
        client->ipaddr.af = AF_INET;
        client->ipaddr.ipaddr.ip4addr.s_addr = INADDR_NONE;
        client->prefix = 0;
        client->longname = client->shortname = data->filename;
        client->secret = client->shortname;
-       client->nastype = "none";
+       client->nastype = strdup("none");
 
        return 0;
 }
+#endif