| /* ----------------------------------------------------------------------- * |
| * |
| * Copyright 2001-2007 H. Peter Anvin - All Rights Reserved |
| * |
| * This program is free software available under the same license |
| * as the "OpenBSD" operating system, distributed at |
| * http://www.openbsd.org/. |
| * |
| * ----------------------------------------------------------------------- */ |
| |
| /* |
| * remap.c |
| * |
| * Perform regular-expression based filename remapping. |
| */ |
| |
| #include "config.h" /* Must be included first! */ |
| #include <ctype.h> |
| #include <syslog.h> |
| #include <regex.h> |
| |
| #include "tftpd.h" |
| #include "remap.h" |
| |
| #define DEADMAN_MAX_STEPS 1024 /* Timeout after this many steps */ |
| #define MAXLINE 16384 /* Truncate a line at this many bytes */ |
| |
| #define RULE_REWRITE 0x01 /* This is a rewrite rule */ |
| #define RULE_GLOBAL 0x02 /* Global rule (repeat until no match) */ |
| #define RULE_EXIT 0x04 /* Exit after matching this rule */ |
| #define RULE_RESTART 0x08 /* Restart at the top after matching this rule */ |
| #define RULE_ABORT 0x10 /* Terminate processing with an error */ |
| #define RULE_GETONLY 0x20 /* Applicable to GET only */ |
| #define RULE_PUTONLY 0x40 /* Applicable to PUT only */ |
| #define RULE_INVERSE 0x80 /* Execute if regex *doesn't* match */ |
| |
| struct rule { |
| struct rule *next; |
| int nrule; |
| int rule_flags; |
| regex_t rx; |
| const char *pattern; |
| }; |
| |
| static int xform_null(int c) |
| { |
| return c; |
| } |
| |
| static int xform_toupper(int c) |
| { |
| return toupper(c); |
| } |
| |
| static int xform_tolower(int c) |
| { |
| return tolower(c); |
| } |
| |
| /* Do \-substitution. Call with string == NULL to get length only. */ |
| static int genmatchstring(char *string, const char *pattern, |
| const char *input, const regmatch_t * pmatch, |
| match_pattern_callback macrosub) |
| { |
| int (*xform) (int) = xform_null; |
| int len = 0; |
| int n, mlen, sublen; |
| int endbytes; |
| |
| /* Get section before match; note pmatch[0] is the whole match */ |
| endbytes = strlen(input) - pmatch[0].rm_eo; |
| len = pmatch[0].rm_so + endbytes; |
| if (string) { |
| memcpy(string, input, pmatch[0].rm_so); |
| string += pmatch[0].rm_so; |
| } |
| |
| /* Transform matched section */ |
| while (*pattern) { |
| mlen = 0; |
| |
| if (*pattern == '\\' && pattern[1] != '\0') { |
| char macro = pattern[1]; |
| switch (macro) { |
| case '0': |
| case '1': |
| case '2': |
| case '3': |
| case '4': |
| case '5': |
| case '6': |
| case '7': |
| case '8': |
| case '9': |
| n = pattern[1] - '0'; |
| |
| if (pmatch[n].rm_so != -1) { |
| mlen = pmatch[n].rm_eo - pmatch[n].rm_so; |
| len += mlen; |
| if (string) { |
| const char *p = input + pmatch[n].rm_so; |
| while (mlen--) |
| *string++ = xform(*p++); |
| } |
| } |
| break; |
| |
| case 'L': |
| xform = xform_tolower; |
| break; |
| |
| case 'U': |
| xform = xform_toupper; |
| break; |
| |
| case 'E': |
| xform = xform_null; |
| break; |
| |
| default: |
| if (macrosub && (sublen = macrosub(macro, string)) >= 0) { |
| while (sublen--) { |
| len++; |
| if (string) { |
| *string = xform(*string); |
| string++; |
| } |
| } |
| } else { |
| len++; |
| if (string) |
| *string++ = xform(pattern[1]); |
| } |
| } |
| pattern += 2; |
| } else { |
| len++; |
| if (string) |
| *string++ = xform(*pattern); |
| pattern++; |
| } |
| } |
| |
| /* Copy section after match */ |
| if (string) { |
| memcpy(string, input + pmatch[0].rm_eo, endbytes); |
| string[endbytes] = '\0'; |
| } |
| |
| return len; |
| } |
| |
| /* |
| * Extract a string terminated by non-escaped whitespace; ignoring |
| * leading whitespace. Consider an unescaped # to be a comment marker, |
| * functionally \n. |
| */ |
| static int readescstring(char *buf, char **str) |
| { |
| char *p = *str; |
| int wasbs = 0, len = 0; |
| |
| while (*p && isspace(*p)) |
| p++; |
| |
| if (!*p) { |
| *buf = '\0'; |
| *str = p; |
| return 0; |
| } |
| |
| while (*p) { |
| if (!wasbs && (isspace(*p) || *p == '#')) { |
| *buf = '\0'; |
| *str = p; |
| return len; |
| } |
| /* Important: two backslashes leave us in the !wasbs state! */ |
| wasbs = !wasbs && (*p == '\\'); |
| *buf++ = *p++; |
| len++; |
| } |
| |
| *buf = '\0'; |
| *str = p; |
| return len; |
| } |
| |
| /* Parse a line into a set of instructions */ |
| static int parseline(char *line, struct rule *r, int lineno) |
| { |
| char buffer[MAXLINE]; |
| char *p; |
| int rv; |
| int rxflags = REG_EXTENDED; |
| static int nrule; |
| |
| memset(r, 0, sizeof *r); |
| r->nrule = nrule; |
| |
| if (!readescstring(buffer, &line)) |
| return 0; /* No rule found */ |
| |
| for (p = buffer; *p; p++) { |
| switch (*p) { |
| case 'r': |
| r->rule_flags |= RULE_REWRITE; |
| break; |
| case 'g': |
| r->rule_flags |= RULE_GLOBAL; |
| break; |
| case 'e': |
| r->rule_flags |= RULE_EXIT; |
| break; |
| case 's': |
| r->rule_flags |= RULE_RESTART; |
| break; |
| case 'a': |
| r->rule_flags |= RULE_ABORT; |
| break; |
| case 'i': |
| rxflags |= REG_ICASE; |
| break; |
| case 'G': |
| r->rule_flags |= RULE_GETONLY; |
| break; |
| case 'P': |
| r->rule_flags |= RULE_PUTONLY; |
| break; |
| case '~': |
| r->rule_flags |= RULE_INVERSE; |
| break; |
| default: |
| syslog(LOG_ERR, |
| "Remap command \"%s\" on line %d contains invalid char \"%c\"", |
| buffer, lineno, *p); |
| return -1; /* Error */ |
| break; |
| } |
| } |
| |
| /* RULE_GLOBAL only applies when RULE_REWRITE specified */ |
| if (!(r->rule_flags & RULE_REWRITE)) |
| r->rule_flags &= ~RULE_GLOBAL; |
| |
| if ((r->rule_flags & (RULE_INVERSE | RULE_REWRITE)) == |
| (RULE_INVERSE | RULE_REWRITE)) { |
| syslog(LOG_ERR, "r rules cannot be inverted, line %d: %s\n", |
| lineno, line); |
| return -1; /* Error */ |
| } |
| |
| /* Read and compile the regex */ |
| if (!readescstring(buffer, &line)) { |
| syslog(LOG_ERR, "No regex on remap line %d: %s\n", lineno, line); |
| return -1; /* Error */ |
| } |
| |
| if ((rv = regcomp(&r->rx, buffer, rxflags)) != 0) { |
| char errbuf[BUFSIZ]; |
| regerror(rv, &r->rx, errbuf, BUFSIZ); |
| syslog(LOG_ERR, "Bad regex in remap line %d: %s\n", lineno, |
| errbuf); |
| return -1; /* Error */ |
| } |
| |
| /* Read the rewrite pattern, if any */ |
| if (readescstring(buffer, &line)) { |
| r->pattern = tfstrdup(buffer); |
| } else { |
| r->pattern = ""; |
| } |
| |
| nrule++; |
| return 1; /* Rule found */ |
| } |
| |
| /* Read a rule file */ |
| struct rule *parserulefile(FILE * f) |
| { |
| char line[MAXLINE]; |
| struct rule *first_rule = NULL; |
| struct rule **last_rule = &first_rule; |
| struct rule *this_rule = tfmalloc(sizeof(struct rule)); |
| int rv; |
| int lineno = 0; |
| int err = 0; |
| |
| while (lineno++, fgets(line, MAXLINE, f)) { |
| rv = parseline(line, this_rule, lineno); |
| if (rv < 0) |
| err = 1; |
| if (rv > 0) { |
| *last_rule = this_rule; |
| last_rule = &this_rule->next; |
| this_rule = tfmalloc(sizeof(struct rule)); |
| } |
| } |
| |
| free(this_rule); /* Last one is always unused */ |
| |
| if (err) { |
| /* Bail on error, we have already logged an error message */ |
| exit(EX_CONFIG); |
| } |
| |
| return first_rule; |
| } |
| |
| /* Destroy a rule file data structure */ |
| void freerules(struct rule *r) |
| { |
| struct rule *next; |
| |
| while (r) { |
| next = r->next; |
| |
| regfree(&r->rx); |
| |
| /* "" patterns aren't allocated by malloc() */ |
| if (r->pattern && *r->pattern) |
| free((void *)r->pattern); |
| |
| free(r); |
| |
| r = next; |
| } |
| } |
| |
| /* Execute a rule set on a string; returns a malloc'd new string. */ |
| char *rewrite_string(const char *input, const struct rule *rules, |
| int is_put, match_pattern_callback macrosub, |
| const char **errmsg) |
| { |
| char *current = tfstrdup(input); |
| char *newstr; |
| const struct rule *ruleptr = rules; |
| regmatch_t pmatch[10]; |
| int len; |
| int was_match = 0; |
| int deadman = DEADMAN_MAX_STEPS; |
| |
| /* Default error */ |
| *errmsg = "Remap table failure"; |
| |
| if (verbosity >= 3) { |
| syslog(LOG_INFO, "remap: input: %s", current); |
| } |
| |
| for (ruleptr = rules; ruleptr; ruleptr = ruleptr->next) { |
| if (((ruleptr->rule_flags & RULE_GETONLY) && is_put) || |
| ((ruleptr->rule_flags & RULE_PUTONLY) && !is_put)) { |
| continue; /* Rule not applicable, try next */ |
| } |
| |
| if (!deadman--) { |
| syslog(LOG_WARNING, |
| "remap: Breaking loop, input = %s, last = %s", input, |
| current); |
| free(current); |
| return NULL; /* Did not terminate! */ |
| } |
| |
| do { |
| if (regexec(&ruleptr->rx, current, 10, pmatch, 0) == |
| (ruleptr->rule_flags & RULE_INVERSE ? REG_NOMATCH : 0)) { |
| /* Match on this rule */ |
| was_match = 1; |
| |
| if (ruleptr->rule_flags & RULE_INVERSE) { |
| /* No actual match, so clear out the pmatch array */ |
| int i; |
| for (i = 0; i < 10; i++) |
| pmatch[i].rm_so = pmatch[i].rm_eo = -1; |
| } |
| |
| if (ruleptr->rule_flags & RULE_ABORT) { |
| if (verbosity >= 3) { |
| syslog(LOG_INFO, "remap: rule %d: abort: %s", |
| ruleptr->nrule, current); |
| } |
| if (ruleptr->pattern[0]) { |
| /* Custom error message */ |
| len = |
| genmatchstring(NULL, ruleptr->pattern, current, |
| pmatch, macrosub); |
| newstr = tfmalloc(len + 1); |
| genmatchstring(newstr, ruleptr->pattern, current, |
| pmatch, macrosub); |
| *errmsg = newstr; |
| } else { |
| *errmsg = NULL; |
| } |
| free(current); |
| return (NULL); |
| } |
| |
| if (ruleptr->rule_flags & RULE_REWRITE) { |
| len = genmatchstring(NULL, ruleptr->pattern, current, |
| pmatch, macrosub); |
| newstr = tfmalloc(len + 1); |
| genmatchstring(newstr, ruleptr->pattern, current, |
| pmatch, macrosub); |
| free(current); |
| current = newstr; |
| if (verbosity >= 3) { |
| syslog(LOG_INFO, "remap: rule %d: rewrite: %s", |
| ruleptr->nrule, current); |
| } |
| } |
| } else { |
| break; /* No match, terminate unconditionally */ |
| } |
| /* If the rule is global, keep going until no match */ |
| } while (ruleptr->rule_flags & RULE_GLOBAL); |
| |
| if (was_match) { |
| was_match = 0; |
| |
| if (ruleptr->rule_flags & RULE_EXIT) { |
| if (verbosity >= 3) { |
| syslog(LOG_INFO, "remap: rule %d: exit", |
| ruleptr->nrule); |
| } |
| return current; /* Exit here, we're done */ |
| } else if (ruleptr->rule_flags & RULE_RESTART) { |
| ruleptr = rules; /* Start from the top */ |
| if (verbosity >= 3) { |
| syslog(LOG_INFO, "remap: rule %d: restart", |
| ruleptr->nrule); |
| } |
| } |
| } |
| } |
| |
| if (verbosity >= 3) { |
| syslog(LOG_INFO, "remap: done"); |
| } |
| return current; |
| } |