blob: cdb90629333922cacc4a022676a7fa612dc85136 [file] [log] [blame] [edit]
/* ----------------------------------------------------------------------- *
*
* 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;
}