tftpd: allow IPv4/6-specific remapping rules
[tftp-hpa.git] / tftpd / remap.c
blob6f5b409ac8a6e2a47607e7f3f89cc4b0d13c530f
1 /* ----------------------------------------------------------------------- *
3 * Copyright 2001-2014 H. Peter Anvin - All Rights Reserved
5 * This program is free software available under the same license
6 * as the "OpenBSD" operating system, distributed at
7 * http://www.openbsd.org/.
9 * ----------------------------------------------------------------------- */
12 * remap.c
14 * Perform regular-expression based filename remapping.
17 #include "config.h" /* Must be included first! */
18 #include <ctype.h>
19 #include <syslog.h>
20 #include <regex.h>
22 #include "tftpd.h"
23 #include "remap.h"
25 #define DEADMAN_MAX_STEPS 1024 /* Timeout after this many steps */
26 #define MAXLINE 16384 /* Truncate a line at this many bytes */
28 #define RULE_REWRITE 0x01 /* This is a rewrite rule */
29 #define RULE_GLOBAL 0x02 /* Global rule (repeat until no match) */
30 #define RULE_EXIT 0x04 /* Exit after matching this rule */
31 #define RULE_RESTART 0x08 /* Restart at the top after matching this rule */
32 #define RULE_ABORT 0x10 /* Terminate processing with an error */
33 #define RULE_INVERSE 0x20 /* Execute if regex *doesn't* match */
34 #define RULE_IPV4 0x40 /* IPv4 only */
35 #define RULE_IPV6 0x80 /* IPv6 only */
37 struct rule {
38 struct rule *next;
39 int nrule;
40 int rule_flags;
41 char rule_mode;
42 regex_t rx;
43 const char *pattern;
46 static int xform_null(int c)
48 return c;
51 static int xform_toupper(int c)
53 return toupper(c);
56 static int xform_tolower(int c)
58 return tolower(c);
61 /* Do \-substitution. Call with string == NULL to get length only. */
62 static int genmatchstring(char *string, const char *pattern,
63 const char *input, const regmatch_t * pmatch,
64 match_pattern_callback macrosub)
66 int (*xform) (int) = xform_null;
67 int len = 0;
68 int n, mlen, sublen;
69 int endbytes;
71 /* Get section before match; note pmatch[0] is the whole match */
72 endbytes = strlen(input) - pmatch[0].rm_eo;
73 len = pmatch[0].rm_so + endbytes;
74 if (string) {
75 memcpy(string, input, pmatch[0].rm_so);
76 string += pmatch[0].rm_so;
79 /* Transform matched section */
80 while (*pattern) {
81 mlen = 0;
83 if (*pattern == '\\' && pattern[1] != '\0') {
84 char macro = pattern[1];
85 switch (macro) {
86 case '0':
87 case '1':
88 case '2':
89 case '3':
90 case '4':
91 case '5':
92 case '6':
93 case '7':
94 case '8':
95 case '9':
96 n = pattern[1] - '0';
98 if (pmatch[n].rm_so != -1) {
99 mlen = pmatch[n].rm_eo - pmatch[n].rm_so;
100 len += mlen;
101 if (string) {
102 const char *p = input + pmatch[n].rm_so;
103 while (mlen--)
104 *string++ = xform(*p++);
107 break;
109 case 'L':
110 xform = xform_tolower;
111 break;
113 case 'U':
114 xform = xform_toupper;
115 break;
117 case 'E':
118 xform = xform_null;
119 break;
121 default:
122 if (macrosub && (sublen = macrosub(macro, string)) >= 0) {
123 while (sublen--) {
124 len++;
125 if (string) {
126 *string = xform(*string);
127 string++;
130 } else {
131 len++;
132 if (string)
133 *string++ = xform(pattern[1]);
136 pattern += 2;
137 } else {
138 len++;
139 if (string)
140 *string++ = xform(*pattern);
141 pattern++;
145 /* Copy section after match */
146 if (string) {
147 memcpy(string, input + pmatch[0].rm_eo, endbytes);
148 string[endbytes] = '\0';
151 return len;
155 * Extract a string terminated by non-escaped whitespace; ignoring
156 * leading whitespace. Consider an unescaped # to be a comment marker,
157 * functionally \n.
159 static int readescstring(char *buf, char **str)
161 char *p = *str;
162 int wasbs = 0, len = 0;
164 while (*p && isspace(*p))
165 p++;
167 if (!*p) {
168 *buf = '\0';
169 *str = p;
170 return 0;
173 while (*p) {
174 if (!wasbs && (isspace(*p) || *p == '#')) {
175 *buf = '\0';
176 *str = p;
177 return len;
179 /* Important: two backslashes leave us in the !wasbs state! */
180 wasbs = !wasbs && (*p == '\\');
181 *buf++ = *p++;
182 len++;
185 *buf = '\0';
186 *str = p;
187 return len;
190 /* Parse a line into a set of instructions */
191 static int parseline(char *line, struct rule *r, int lineno)
193 char buffer[MAXLINE];
194 char *p;
195 int rv;
196 int rxflags = REG_EXTENDED;
197 static int nrule;
199 memset(r, 0, sizeof *r);
200 r->nrule = nrule;
202 if (!readescstring(buffer, &line))
203 return 0; /* No rule found */
205 for (p = buffer; *p; p++) {
206 switch (*p) {
207 case 'r':
208 r->rule_flags |= RULE_REWRITE;
209 break;
210 case 'g':
211 r->rule_flags |= RULE_GLOBAL;
212 break;
213 case 'e':
214 r->rule_flags |= RULE_EXIT;
215 break;
216 case 's':
217 r->rule_flags |= RULE_RESTART;
218 break;
219 case 'a':
220 r->rule_flags |= RULE_ABORT;
221 break;
222 case 'i':
223 rxflags |= REG_ICASE;
224 break;
225 case '~':
226 r->rule_flags |= RULE_INVERSE;
227 break;
228 case '4':
229 r->rule_flags |= RULE_IPV4;
230 break;
231 case '6':
232 r->rule_flags |= RULE_IPV6;
233 break;
234 case 'G':
235 case 'P':
236 r->rule_mode = *p;
237 break;
238 default:
239 syslog(LOG_ERR,
240 "Remap command \"%s\" on line %d contains invalid char \"%c\"",
241 buffer, lineno, *p);
242 return -1; /* Error */
243 break;
247 /* RULE_GLOBAL only applies when RULE_REWRITE specified */
248 if (!(r->rule_flags & RULE_REWRITE))
249 r->rule_flags &= ~RULE_GLOBAL;
251 if ((r->rule_flags & (RULE_INVERSE | RULE_REWRITE)) ==
252 (RULE_INVERSE | RULE_REWRITE)) {
253 syslog(LOG_ERR, "r rules cannot be inverted, line %d: %s\n",
254 lineno, line);
255 return -1; /* Error */
258 /* Read and compile the regex */
259 if (!readescstring(buffer, &line)) {
260 syslog(LOG_ERR, "No regex on remap line %d: %s\n", lineno, line);
261 return -1; /* Error */
264 if ((rv = regcomp(&r->rx, buffer, rxflags)) != 0) {
265 char errbuf[BUFSIZ];
266 regerror(rv, &r->rx, errbuf, BUFSIZ);
267 syslog(LOG_ERR, "Bad regex in remap line %d: %s\n", lineno,
268 errbuf);
269 return -1; /* Error */
272 /* Read the rewrite pattern, if any */
273 if (readescstring(buffer, &line)) {
274 r->pattern = tfstrdup(buffer);
275 } else {
276 r->pattern = "";
279 nrule++;
280 return 1; /* Rule found */
283 /* Read a rule file */
284 struct rule *parserulefile(FILE * f)
286 char line[MAXLINE];
287 struct rule *first_rule = NULL;
288 struct rule **last_rule = &first_rule;
289 struct rule *this_rule = tfmalloc(sizeof(struct rule));
290 int rv;
291 int lineno = 0;
292 int err = 0;
294 while (lineno++, fgets(line, MAXLINE, f)) {
295 rv = parseline(line, this_rule, lineno);
296 if (rv < 0)
297 err = 1;
298 if (rv > 0) {
299 *last_rule = this_rule;
300 last_rule = &this_rule->next;
301 this_rule = tfmalloc(sizeof(struct rule));
305 free(this_rule); /* Last one is always unused */
307 if (err) {
308 /* Bail on error, we have already logged an error message */
309 exit(EX_CONFIG);
312 return first_rule;
315 /* Destroy a rule file data structure */
316 void freerules(struct rule *r)
318 struct rule *next;
320 while (r) {
321 next = r->next;
323 regfree(&r->rx);
325 /* "" patterns aren't allocated by malloc() */
326 if (r->pattern && *r->pattern)
327 free((void *)r->pattern);
329 free(r);
331 r = next;
335 /* Execute a rule set on a string; returns a malloc'd new string. */
336 char *rewrite_string(const char *input, const struct rule *rules,
337 char mode, int af, match_pattern_callback macrosub,
338 const char **errmsg)
340 char *current = tfstrdup(input);
341 char *newstr;
342 const struct rule *ruleptr = rules;
343 regmatch_t pmatch[10];
344 int len;
345 int was_match = 0;
346 int deadman = DEADMAN_MAX_STEPS;
348 /* Default error */
349 *errmsg = "Remap table failure";
351 if (verbosity >= 3) {
352 syslog(LOG_INFO, "remap: input: %s", current);
355 for (ruleptr = rules; ruleptr; ruleptr = ruleptr->next) {
356 if (ruleptr->rule_mode && ruleptr->rule_mode != mode)
357 continue; /* Rule not applicable, try next */
359 if ((ruleptr->rule_flags & RULE_IPV4) && (af != AF_INET))
360 continue; /* Rule not applicable, try next */
362 if ((ruleptr->rule_flags & RULE_IPV6) && (af != AF_INET6))
363 continue; /* Rule not applicable, try next */
365 if (!deadman--) {
366 syslog(LOG_WARNING,
367 "remap: Breaking loop, input = %s, last = %s", input,
368 current);
369 free(current);
370 return NULL; /* Did not terminate! */
373 do {
374 if (regexec(&ruleptr->rx, current, 10, pmatch, 0) ==
375 (ruleptr->rule_flags & RULE_INVERSE ? REG_NOMATCH : 0)) {
376 /* Match on this rule */
377 was_match = 1;
379 if (ruleptr->rule_flags & RULE_INVERSE) {
380 /* No actual match, so clear out the pmatch array */
381 int i;
382 for (i = 0; i < 10; i++)
383 pmatch[i].rm_so = pmatch[i].rm_eo = -1;
386 if (ruleptr->rule_flags & RULE_ABORT) {
387 if (verbosity >= 3) {
388 syslog(LOG_INFO, "remap: rule %d: abort: %s",
389 ruleptr->nrule, current);
391 if (ruleptr->pattern[0]) {
392 /* Custom error message */
393 len =
394 genmatchstring(NULL, ruleptr->pattern, current,
395 pmatch, macrosub);
396 newstr = tfmalloc(len + 1);
397 genmatchstring(newstr, ruleptr->pattern, current,
398 pmatch, macrosub);
399 *errmsg = newstr;
400 } else {
401 *errmsg = NULL;
403 free(current);
404 return (NULL);
407 if (ruleptr->rule_flags & RULE_REWRITE) {
408 len = genmatchstring(NULL, ruleptr->pattern, current,
409 pmatch, macrosub);
410 newstr = tfmalloc(len + 1);
411 genmatchstring(newstr, ruleptr->pattern, current,
412 pmatch, macrosub);
413 free(current);
414 current = newstr;
415 if (verbosity >= 3) {
416 syslog(LOG_INFO, "remap: rule %d: rewrite: %s",
417 ruleptr->nrule, current);
420 } else {
421 break; /* No match, terminate unconditionally */
423 /* If the rule is global, keep going until no match */
424 } while (ruleptr->rule_flags & RULE_GLOBAL);
426 if (was_match) {
427 was_match = 0;
429 if (ruleptr->rule_flags & RULE_EXIT) {
430 if (verbosity >= 3) {
431 syslog(LOG_INFO, "remap: rule %d: exit",
432 ruleptr->nrule);
434 return current; /* Exit here, we're done */
435 } else if (ruleptr->rule_flags & RULE_RESTART) {
436 ruleptr = rules; /* Start from the top */
437 if (verbosity >= 3) {
438 syslog(LOG_INFO, "remap: rule %d: restart",
439 ruleptr->nrule);
445 if (verbosity >= 3) {
446 syslog(LOG_INFO, "remap: done");
448 return current;