rlm_otp import from HEAD
[freeradius.git] / src / main / xlat.c
1 /*
2  * xlat.c       Translate strings.  This is the first version of xlat
3  *              incorporated to RADIUS
4  *
5  * Version:     $Id$
6  *
7  *   This program is free software; you can redistribute it and/or modify
8  *   it under the terms of the GNU General Public License as published by
9  *   the Free Software Foundation; either version 2 of the License, or
10  *   (at your option) any later version.
11  *
12  *   This program is distributed in the hope that it will be useful,
13  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
14  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  *   GNU General Public License for more details.
16  *
17  *   You should have received a copy of the GNU General Public License
18  *   along with this program; if not, write to the Free Software
19  *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20  *
21  * Copyright 2000  The FreeRADIUS server project
22  * Copyright 2000  Alan DeKok <aland@ox.org>
23  */
24
25 static const char rcsid[] =
26 "$Id$";
27
28 #include        "autoconf.h"
29 #include        "libradius.h"
30
31 #include        <stdio.h>
32 #include        <stdlib.h>
33 #include        <string.h>
34 #include        <ctype.h>
35
36 #include        "radiusd.h"
37
38 #include        "rad_assert.h"
39
40 typedef struct xlat_t {
41         char            module[MAX_STRING_LEN];
42         int             length;
43         void            *instance;
44         RAD_XLAT_FUNC   do_xlat;
45         int             internal;       /* not allowed to re-define these */
46 } xlat_t;
47
48 static rbtree_t *xlat_root = NULL;
49
50 /*
51  *      Define all xlat's in the structure.
52  */
53 static const char *internal_xlat[] = {"check",
54                                       "request",
55                                       "reply",
56                                       "proxy-request",
57                                       "proxy-reply",
58                                       NULL};
59
60 #if REQUEST_MAX_REGEX > 8
61 #error Please fix the following line
62 #endif
63 static int xlat_inst[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8 }; /* up to 8 for regex */
64
65
66 /*
67  *      Convert the value on a VALUE_PAIR to string
68  */
69 static int valuepair2str(char * out,int outlen,VALUE_PAIR * pair,
70                          int type, RADIUS_ESCAPE_STRING func)
71 {
72         char buffer[MAX_STRING_LEN * 4];
73
74         if (pair != NULL) {
75                 vp_prints_value(buffer, sizeof(buffer), pair, 0);
76                 return func(out, outlen, buffer);
77         }
78
79         switch (type) {
80         case PW_TYPE_STRING :
81                 strNcpy(out,"_",outlen);
82                 break;
83         case PW_TYPE_INTEGER :
84                 strNcpy(out,"0",outlen);
85                 break;
86         case PW_TYPE_IPADDR :
87                 strNcpy(out,"?.?.?.?",outlen);
88                 break;
89         case PW_TYPE_DATE :
90                 strNcpy(out,"0",outlen);
91                 break;
92         default :
93                 strNcpy(out,"unknown_type",outlen);
94         }
95         return strlen(out);
96 }
97
98
99 /*
100  *      Dynamically translate for check:, request:, reply:, etc.
101  */
102 static int xlat_packet(void *instance, REQUEST *request,
103                        char *fmt, char *out, size_t outlen,
104                        RADIUS_ESCAPE_STRING func)
105 {
106         DICT_ATTR       *da;
107         VALUE_PAIR      *vp;
108         VALUE_PAIR      *vps = NULL;
109         RADIUS_PACKET   *packet = NULL;
110
111         switch (*(int*) instance) {
112         case 0:
113                 vps = request->config_items;
114                 break;
115
116         case 1:
117                 vps = request->packet->vps;
118                 packet = request->packet;
119                 break;
120
121         case 2:
122                 vps = request->reply->vps;
123                 packet = request->reply;
124                 break;
125
126         case 3:
127                 if (request->proxy) vps = request->proxy->vps;
128                 packet = request->proxy;
129                 break;
130
131         case 4:
132                 if (request->proxy_reply) vps = request->proxy_reply->vps;
133                 packet = request->proxy_reply;
134                 break;
135
136         default:                /* WTF? */
137                 return 0;
138         }
139
140         /*
141          *      The "format" string is the attribute name.
142          */
143         da = dict_attrbyname(fmt);
144         if (!da) return 0;
145
146         vp = pairfind(vps, da->attr);
147         if (!vp) {
148                 /*
149                  *      Some "magic" handlers, which are never in VP's, but
150                  *      which are in the packet.
151                  *
152                  *      FIXME: Add SRC/DST IP address!
153                  */
154                 if (packet) {
155                         switch (da->attr) {
156                         case PW_PACKET_TYPE:
157                         {
158                                 DICT_VALUE *dval;
159                                 
160                                 dval = dict_valbyattr(da->attr, packet->code);
161                                 if (dval) {
162                                         snprintf(out, outlen, "%s", dval->name);
163                                 } else {
164                                         snprintf(out, outlen, "%d", packet->code);
165                                 }
166                                 return strlen(out);
167                         }
168                         break;
169                         
170                         default:
171                                 break;
172                         }
173                 }
174
175                 /*
176                  *      Not found, die.
177                  */
178                 return 0;
179         }
180
181         if (!vps) return 0;     /* silently fail */
182
183         /*
184          *      Convert the VP to a string, and return it.
185          */
186         return valuepair2str(out, outlen, vp, da->type, func);
187 }
188
189 #ifdef HAVE_REGEX_H
190 /*
191  *      Pull %{0} to %{8} out of the packet.
192  */
193 static int xlat_regex(void *instance, REQUEST *request,
194                       char *fmt, char *out, size_t outlen,
195                       RADIUS_ESCAPE_STRING func)
196 {
197         char *regex;
198
199         /*
200          *      We cheat: fmt is "0" to "8", but those numbers
201          *      are already in the "instance".
202          */
203         fmt = fmt;              /* -Wunused */
204         func = func;            /* -Wunused FIXME: do escaping? */
205         
206         regex = request_data_get(request, request,
207                                  REQUEST_DATA_REGEX | *(int *)instance);
208         if (!regex) return 0;
209
210         /*
211          *      Copy UP TO "freespace" bytes, including
212          *      a zero byte.
213          */
214         strNcpy(out, regex, outlen);
215         free(regex); /* was strdup'd */
216         return strlen(out);
217 }
218 #endif                          /* HAVE_REGEX_H */
219
220 /*
221  *      Compare two xlat_t structs, based ONLY on the module name.
222  */
223 static int xlat_cmp(const void *a, const void *b)
224 {
225         if (((const xlat_t *)a)->length != ((const xlat_t *)b)->length) {
226                 return ((const xlat_t *)a)->length - ((const xlat_t *)b)->length;
227         }
228
229         return memcmp(((const xlat_t *)a)->module,
230                       ((const xlat_t *)b)->module,
231                       ((const xlat_t *)a)->length);
232 }
233
234
235 /*
236  *      find the appropriate registered xlat function.
237  */
238 static xlat_t *xlat_find(const char *module)
239 {
240         char *p;
241         xlat_t my_xlat;
242
243         strNcpy(my_xlat.module, module, sizeof(my_xlat.module));
244
245         /*
246          *      We get passed the WHOLE string, and all we want here
247          *      is the first piece.
248          */
249         p = strchr(my_xlat.module, ':');
250         if (p) *p = '\0';
251
252         my_xlat.length = strlen(my_xlat.module);
253
254         return rbtree_finddata(xlat_root, &my_xlat);
255 }
256
257
258 /*
259  *      Register an xlat function.
260  */
261 int xlat_register(const char *module, RAD_XLAT_FUNC func, void *instance)
262 {
263         xlat_t  *c;
264         xlat_t  my_xlat;
265
266         if ((module == NULL) || (strlen(module) == 0)) {
267                 DEBUG("xlat_register: Invalid module name");
268                 return -1;
269         }
270
271         /*
272          *      First time around, build up the tree...
273          *
274          *      FIXME: This code should be hoisted out of this function,
275          *      and into a global "initialization".  But it isn't critical...
276          */
277         if (!xlat_root) {
278                 int i;
279 #ifdef HAVE_REGEX_H
280                 char buffer[2];
281 #endif
282
283                 xlat_root = rbtree_create(xlat_cmp, free, 0);
284                 if (!xlat_root) {
285                         DEBUG("xlat_register: Failed to create tree.");
286                         return -1;
287                 }
288
289                 /*
290                  *      Register the internal packet xlat's.
291                  */
292                 for (i = 0; internal_xlat[i] != NULL; i++) {
293                         xlat_register(internal_xlat[i], xlat_packet, &xlat_inst[i]);
294                         c = xlat_find(internal_xlat[i]);
295                         rad_assert(c != NULL);
296                         c->internal = TRUE;
297                 }
298
299 #ifdef HAVE_REGEX_H
300                 /*
301                  *      Register xlat's for regexes.
302                  */
303                 buffer[1] = '\0';
304                 for (i = 0; i <= REQUEST_MAX_REGEX; i++) {
305                         buffer[0] = '0' + i;
306                         xlat_register(buffer, xlat_regex, &xlat_inst[i]);
307                         c = xlat_find(buffer);
308                         rad_assert(c != NULL);
309                         c->internal = TRUE;
310                 }
311 #endif /* HAVE_REGEX_H */
312         }
313
314         /*
315          *      If it already exists, replace the instance.
316          */
317         strNcpy(my_xlat.module, module, sizeof(my_xlat.module));
318         my_xlat.length = strlen(my_xlat.module);
319         c = rbtree_finddata(xlat_root, &my_xlat);
320         if (c) {
321                 if (c->internal) {
322                         DEBUG("xlat_register: Cannot re-define internal xlat");
323                         return -1;
324                 }
325
326                 c->do_xlat = func;
327                 c->instance = instance;
328                 return 0;
329         }
330
331         /*
332          *      Doesn't exist.  Create it.
333          */
334         c = rad_malloc(sizeof(xlat_t));
335         memset(c, 0, sizeof(*c));
336
337         c->do_xlat = func;
338         strNcpy(c->module, module, sizeof(c->module));
339         c->length = strlen(c->module);
340         c->instance = instance;
341
342         rbtree_insert(xlat_root, c);
343
344         return 0;
345 }
346
347 /*
348  *      Unregister an xlat function.
349  *
350  *      We can only have one function to call per name, so the
351  *      passing of "func" here is extraneous.
352  */
353 void xlat_unregister(const char *module, RAD_XLAT_FUNC func)
354 {
355         rbnode_t        *node;
356         xlat_t          my_xlat;
357
358         func = func;            /* -Wunused */
359
360         strNcpy(my_xlat.module, module, sizeof(my_xlat.module));
361         my_xlat.length = strlen(my_xlat.module);
362
363         node = rbtree_find(xlat_root, &my_xlat);
364         if (!node) return;
365
366         rbtree_delete(xlat_root, node);
367 }
368
369
370 /*
371  *      Decode an attribute name into a string.
372  */
373 static void decode_attribute(const char **from, char **to, int freespace,
374                              int *open, REQUEST *request,
375                              RADIUS_ESCAPE_STRING func)
376 {
377         char attrname[256];
378         const char *p;
379         char *q, *pa;
380         int stop=0, found=0, retlen=0;
381         int openbraces = *open;
382         xlat_t *c;
383
384         p = *from;
385         q = *to;
386         pa = &attrname[0];
387
388         *q = '\0';
389
390         /*
391          * Skip the '{' at the front of 'p'
392          * Increment open braces
393          */
394         p++;
395         openbraces++;
396
397         /*
398          *  Copy over the rest of the string.
399          */
400         while ((*p) && (!stop)) {
401                 switch(*p) {
402                         /*
403                          *  Allow braces inside things, too.
404                          */
405                         case '\\':
406                                 p++; /* skip it */
407                                 *pa++ = *p++;
408                                 break;
409
410                         case '{':
411                                 openbraces++;
412                                 *pa++ = *p++;
413                                 break;
414
415                         case '}':
416                                 openbraces--;
417                                 if (openbraces == *open) {
418                                         p++;
419                                         stop=1;
420                                 } else {
421                                         *pa++ = *p++;
422                                 }
423                                 break;
424
425                                 /*
426                                  *  Attr-Name1:-Attr-Name2
427                                  *
428                                  *  Use Attr-Name1, and if not found,
429                                  *  use Attr-Name2.
430                                  */
431                         case ':':
432                                 if (p[1] == '-') {
433                                         p += 2;
434                                         stop = 1;
435                                         break;
436                                 }
437                                 /* else FALL-THROUGH */
438
439                         default:
440                                 *pa++ = *p++;
441                                 break;
442                 }
443         }
444         *pa = '\0';
445
446         /*
447          *      Look up almost everything in the new tree of xlat
448          *      functions.  this makes it a little quicker...
449          */
450         if ((c = xlat_find(attrname)) != NULL) {
451                 if (!c->internal) DEBUG("radius_xlat: Running registered xlat function of module %s for string \'%s\'",
452                                         c->module, attrname+ c->length + 1);
453                 retlen = c->do_xlat(c->instance, request, attrname+(c->length+1), q, freespace, func);
454                 /* If retlen is 0, treat it as not found */
455                 if (retlen == 0) {
456                         found = 0;
457                 } else {
458                         found = 1;
459                         q += retlen;
460                 }
461
462                 /*
463                  *      Not in the default xlat database.  Must be
464                  *      a bare attribute number.
465                  */
466         } else if ((retlen = xlat_packet(&xlat_inst[1], request, attrname,
467                                          q, freespace, func)) > 0) {
468                 found = 1;
469                 q += retlen;
470
471                 /*
472                  *      Look up the name, in order to get the correct
473                  *      debug message.
474                  */
475 #ifndef NDEBUG
476         } else if (dict_attrbyname(attrname) == NULL) {
477                 /*
478                  *      No attribute by that name, return an error.
479                  */
480                 DEBUG2("WARNING: Attempt to use unknown xlat function, or non-existent attribute in string %%{%s}", attrname);
481 #endif
482         } /* else the attribute is known, but not in the request */
483
484         /*
485          * Skip to last '}' if attr is found
486          * The rest of the stuff within the braces is
487          * useless if we found what we need
488          */
489         if (found) {
490                 while((*p != '\0') && (openbraces > 0)) {
491                         /*
492                          *      Handle escapes outside of the loop.
493                          */
494                         if (*p == '\\') {
495                                 p++;
496                                 if (!*p) break;
497                                 p++; /* get & ignore next character */
498                                 continue;
499                         }
500
501                         switch (*p) {
502                         default:
503                                 break;
504
505                                 /*
506                                  *  Bare brace
507                                  */
508                         case '{':
509                                 openbraces++;
510                                 break;
511
512                         case '}':
513                                 openbraces--;
514                                 break;
515                         }
516                         p++;    /* skip the character */
517                 }
518         }
519
520         *open = openbraces;
521         *from = p;
522         *to = q;
523 }
524
525 /*
526  *  If the caller doesn't pass xlat an escape function, then
527  *  we use this one.  It simplifies the coding, as the check for
528  *  func == NULL only happens once.
529  */
530 static int xlat_copy(char *out, int outlen, const char *in)
531 {
532         int len = 0;
533
534         while (*in) {
535                 /*
536                  *  Truncate, if too much.
537                  */
538                 if (len >= outlen) {
539                         break;
540                 }
541
542                 /*
543                  *  Copy data.
544                  *
545                  *  FIXME: Do escaping of bad stuff!
546                  */
547                 *out = *in;
548
549                 out++;
550                 in++;
551                 len++;
552         }
553
554         *out = '\0';
555         return len;
556 }
557
558 /*
559  *      Replace %<whatever> in a string.
560  *
561  *      See 'doc/variables.txt' for more information.
562  */
563 int radius_xlat(char *out, int outlen, const char *fmt,
564                 REQUEST *request, RADIUS_ESCAPE_STRING func)
565 {
566         int i, c,freespace;
567         const char *p;
568         char *q;
569         VALUE_PAIR *tmp;
570         struct tm *TM, s_TM;
571         char tmpdt[40]; /* For temporary storing of dates */
572         int openbraces=0;
573
574         /*
575          *      Catch bad modules.
576          */
577         if (!fmt || !out || !request) return 0;
578
579         /*
580          *  Ensure that we always have an escaping function.
581          */
582         if (func == NULL) {
583                 func = xlat_copy;
584         }
585
586         q = out;
587         p = fmt;
588         while (*p) {
589                 /* Calculate freespace in output */
590                 freespace = outlen - (q - out);
591                 if (freespace <= 1)
592                         break;
593                 c = *p;
594
595                 if ((c != '%') && (c != '$') && (c != '\\')) {
596                         /*
597                          * We check if we're inside an open brace.  If we are
598                          * then we assume this brace is NOT literal, but is
599                          * a closing brace and apply it
600                          */
601                         if ((c == '}') && openbraces) {
602                                 openbraces--;
603                                 p++; /* skip it */
604                                 continue;
605                         }
606                         *q++ = *p++;
607                         continue;
608                 }
609
610                 /*
611                  *      There's nothing after this character, copy
612                  *      the last '%' or "$' or '\\' over to the output
613                  *      buffer, and exit.
614                  */
615                 if (*++p == '\0') {
616                         *q++ = c;
617                         break;
618                 }
619
620                 if (c == '\\') {
621                         switch(*p) {
622                         case '\\':
623                                 *q++ = *p;
624                                 break;
625                         case 't':
626                                 *q++ = '\t';
627                                 break;
628                         case 'n':
629                                 *q++ = '\n';
630                                 break;
631                         default:
632                                 *q++ = c;
633                                 *q++ = *p;
634                                 break;
635                         }
636                         p++;
637
638                         /*
639                          *      Hmmm... ${User-Name} is a synonym for
640                          *      %{User-Name}.
641                          *
642                          *      Why, exactly?
643                          */
644                 } else if (c == '$') switch(*p) {
645                         case '{': /* Attribute by Name */
646                                 decode_attribute(&p, &q, freespace, &openbraces, request, func);
647                                 break;
648                         default:
649                                 *q++ = c;
650                                 *q++ = *p++;
651                                 break;
652
653                 } else if (c == '%') switch(*p) {
654                         case '{':
655                                 decode_attribute(&p, &q, freespace, &openbraces, request, func);
656                                 break;
657
658                         case '%':
659                                 *q++ = *p++;
660                                 break;
661                         case 'a': /* Protocol: */
662                                 q += valuepair2str(q,freespace,pairfind(request->reply->vps,PW_FRAMED_PROTOCOL),PW_TYPE_INTEGER, func);
663                                 p++;
664                                 break;
665                         case 'c': /* Callback-Number */
666                                 q += valuepair2str(q,freespace,pairfind(request->reply->vps,PW_CALLBACK_NUMBER),PW_TYPE_STRING, func);
667                                 p++;
668                                 break;
669                         case 'd': /* request day */
670                                 TM = localtime_r(&request->timestamp, &s_TM);
671                                 strftime(tmpdt,sizeof(tmpdt),"%d",TM);
672                                 strNcpy(q,tmpdt,freespace);
673                                 q += strlen(q);
674                                 p++;
675                                 break;
676                         case 'f': /* Framed IP address */
677                                 q += valuepair2str(q,freespace,pairfind(request->reply->vps,PW_FRAMED_IP_ADDRESS),PW_TYPE_IPADDR, func);
678                                 p++;
679                                 break;
680                         case 'i': /* Calling station ID */
681                                 q += valuepair2str(q,freespace,pairfind(request->packet->vps,PW_CALLING_STATION_ID),PW_TYPE_STRING, func);
682                                 p++;
683                                 break;
684                         case 'l': /* request timestamp */
685                                 snprintf(tmpdt, sizeof(tmpdt), "%lu",
686                                          (unsigned long) request->timestamp);
687                                 strNcpy(q,tmpdt,freespace);
688                                 q += strlen(q);
689                                 p++;
690                                 break;
691                         case 'm': /* request month */
692                                 TM = localtime_r(&request->timestamp, &s_TM);
693                                 strftime(tmpdt,sizeof(tmpdt),"%m",TM);
694                                 strNcpy(q,tmpdt,freespace);
695                                 q += strlen(q);
696                                 p++;
697                                 break;
698                         case 'n': /* NAS IP address */
699                                 q += valuepair2str(q,freespace,pairfind(request->packet->vps,PW_NAS_IP_ADDRESS),PW_TYPE_IPADDR, func);
700                                 p++;
701                                 break;
702                         case 'p': /* Port number */
703                                 q += valuepair2str(q,freespace,pairfind(request->packet->vps,PW_NAS_PORT),PW_TYPE_INTEGER, func);
704                                 p++;
705                                 break;
706                         case 's': /* Speed */
707                                 q += valuepair2str(q,freespace,pairfind(request->packet->vps,PW_CONNECT_INFO),PW_TYPE_STRING, func);
708                                 p++;
709                                 break;
710                         case 't': /* request timestamp */
711                                 CTIME_R(&request->timestamp, q, freespace);
712                                 q += strlen(q);
713                                 p++;
714                                 break;
715                         case 'u': /* User name */
716                                 q += valuepair2str(q,freespace,pairfind(request->packet->vps,PW_USER_NAME),PW_TYPE_STRING, func);
717                                 p++;
718                                 break;
719                         case 'A': /* radacct_dir */
720                                 strNcpy(q,radacct_dir,freespace-1);
721                                 q += strlen(q);
722                                 p++;
723                                 break;
724                         case 'C': /* ClientName */
725                                 strNcpy(q,client_name(request->packet->src_ipaddr),freespace-1);
726                                 q += strlen(q);
727                                 p++;
728                                 break;
729                         case 'D': /* request date */
730                                 TM = localtime_r(&request->timestamp, &s_TM);
731                                 strftime(tmpdt,sizeof(tmpdt),"%Y%m%d",TM);
732                                 strNcpy(q,tmpdt,freespace);
733                                 q += strlen(q);
734                                 p++;
735                                 break;
736                         case 'H': /* request hour */
737                                 TM = localtime_r(&request->timestamp, &s_TM);
738                                 strftime(tmpdt,sizeof(tmpdt),"%H",TM);
739                                 strNcpy(q,tmpdt,freespace);
740                                 q += strlen(q);
741                                 p++;
742                                 break;
743                         case 'L': /* radlog_dir */
744                                 strNcpy(q,radlog_dir,freespace-1);
745                                 q += strlen(q);
746                                 p++;
747                                 break;
748                         case 'M': /* MTU */
749                                 q += valuepair2str(q,freespace,pairfind(request->reply->vps,PW_FRAMED_MTU),PW_TYPE_INTEGER, func);
750                                 p++;
751                                 break;
752                         case 'R': /* radius_dir */
753                                 strNcpy(q,radius_dir,freespace-1);
754                                 q += strlen(q);
755                                 p++;
756                                 break;
757                         case 'S': /* request timestamp in SQL format*/
758                                 TM = localtime_r(&request->timestamp, &s_TM);
759                                 strftime(tmpdt,sizeof(tmpdt),"%Y-%m-%d %H:%M:%S",TM);
760                                 strNcpy(q,tmpdt,freespace);
761                                 q += strlen(q);
762                                 p++;
763                                 break;
764                         case 'T': /* request timestamp */
765                                 TM = localtime_r(&request->timestamp, &s_TM);
766                                 strftime(tmpdt,sizeof(tmpdt),"%Y-%m-%d-%H.%M.%S.000000",TM);
767                                 strNcpy(q,tmpdt,freespace);
768                                 q += strlen(q);
769                                 p++;
770                                 break;
771                         case 'U': /* Stripped User name */
772                                 q += valuepair2str(q,freespace,pairfind(request->packet->vps,PW_STRIPPED_USER_NAME),PW_TYPE_STRING, func);
773                                 p++;
774                                 break;
775                         case 'V': /* Request-Authenticator */
776                                 if (request->packet->verified)
777                                         strNcpy(q,"Verified",freespace-1);
778                                 else
779                                         strNcpy(q,"None",freespace-1);
780                                 q += strlen(q);
781                                 p++;
782                                 break;
783                         case 'Y': /* request year */
784                                 TM = localtime_r(&request->timestamp, &s_TM);
785                                 strftime(tmpdt,sizeof(tmpdt),"%Y",TM);
786                                 strNcpy(q,tmpdt,freespace);
787                                 q += strlen(q);
788                                 p++;
789                                 break;
790                         case 'Z': /* Full request pairs except password */
791                                 tmp = request->packet->vps;
792                                 while (tmp && (freespace > 3)) {
793                                         if (tmp->attribute != PW_PASSWORD) {
794                                                 *q++ = '\t';
795                                                 i = vp_prints(q,freespace-2,tmp);
796                                                 q += i;
797                                                 freespace -= (i+2);
798                                                 *q++ = '\n';
799                                         }
800                                         tmp = tmp->next;
801                                 }
802                                 p++;
803                                 break;
804                         default:
805                                 DEBUG2("WARNING: Unknown variable '%%%c': See 'doc/variables.txt'", *p);
806                                 if (freespace > 2) {
807                                         *q++ = '%';
808                                         *q++ = *p++;
809                                 }
810                                 break;
811                 }
812         }
813         *q = '\0';
814
815         DEBUG2("radius_xlat:  '%s'", out);
816
817         return strlen(out);
818 }