forked from mirrors/tftp-hpa-google

Well, now it is actually being compiled, and should hopefully work again... Signed-off-by: H. Peter Anvin <hpa@zytor.com>
545 lines
16 KiB
C
545 lines
16 KiB
C
/* ----------------------------------------------------------------------- *
|
|
*
|
|
* Copyright 2001-2024 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 4096 /* 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_INVERSE 0x20 /* Execute if regex *doesn't* match */
|
|
#define RULE_IPV4 0x40 /* IPv4 only */
|
|
#define RULE_IPV6 0x80 /* IPv6 only */
|
|
|
|
#define RULE_HASFILE 0x100 /* Valid if rule results in a valid filename */
|
|
#define RULE_RRQ 0x200 /* Get (read) only */
|
|
#define RULE_WRQ 0x400 /* Put (write) only */
|
|
#define RULE_SEDG 0x800 /* sed-style global */
|
|
|
|
int deadman_max_steps = DEADMAN_MAX_STEPS;
|
|
|
|
struct rule {
|
|
struct rule *next;
|
|
int nrule;
|
|
unsigned 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.
|
|
* "start" indicates an offset into the input buffer where the pattern
|
|
* match was started.
|
|
*/
|
|
static int do_genmatchstring(char *string, const char *pattern,
|
|
const char *ibuf,
|
|
const regmatch_t * pmatch,
|
|
match_pattern_callback macrosub,
|
|
int start, int *nextp)
|
|
{
|
|
int (*xform) (int) = xform_null;
|
|
int len = 0;
|
|
int n, mlen, sublen;
|
|
int endbytes;
|
|
const char *input = ibuf + start;
|
|
|
|
/* Get section before match; note pmatch[0] is the whole match */
|
|
endbytes = strlen(input) - pmatch[0].rm_eo;
|
|
len = start + pmatch[0].rm_so;
|
|
if (string) {
|
|
/* Copy the prefix before "start" as well! */
|
|
memcpy(string, ibuf, start + pmatch[0].rm_so);
|
|
string += start + 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 + start + 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++;
|
|
}
|
|
}
|
|
|
|
/* Pointer to post-substitution tail */
|
|
if (nextp)
|
|
*nextp = len;
|
|
|
|
/* Copy section after match */
|
|
len += endbytes;
|
|
if (string) {
|
|
memcpy(string, input + pmatch[0].rm_eo, endbytes);
|
|
string[endbytes] = '\0';
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
/*
|
|
* Ditto, but allocate the string in a new buffer
|
|
*/
|
|
|
|
static int genmatchstring(char **string, const char *pattern,
|
|
const char *ibuf,
|
|
const regmatch_t * pmatch,
|
|
match_pattern_callback macrosub,
|
|
int start, int *nextp)
|
|
{
|
|
int len;
|
|
char *buf;
|
|
|
|
len = do_genmatchstring(NULL, pattern, ibuf, pmatch,
|
|
macrosub, start, NULL);
|
|
*string = buf = tfmalloc(len + 1);
|
|
return do_genmatchstring(buf, pattern, ibuf, pmatch,
|
|
macrosub, start, nextp);
|
|
}
|
|
|
|
/*
|
|
* 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':
|
|
if (r->rule_flags & RULE_GLOBAL)
|
|
r->rule_flags |= RULE_SEDG;
|
|
else
|
|
r->rule_flags |= RULE_GLOBAL;
|
|
break;
|
|
case 'e':
|
|
r->rule_flags |= RULE_EXIT;
|
|
break;
|
|
case 'E':
|
|
r->rule_flags |= RULE_HASFILE;
|
|
break;
|
|
case 's':
|
|
r->rule_flags |= RULE_RESTART;
|
|
break;
|
|
case 'a':
|
|
r->rule_flags |= RULE_ABORT;
|
|
break;
|
|
case 'i':
|
|
rxflags |= REG_ICASE;
|
|
break;
|
|
case '~':
|
|
r->rule_flags |= RULE_INVERSE;
|
|
break;
|
|
case '4':
|
|
r->rule_flags |= RULE_IPV4;
|
|
break;
|
|
case '6':
|
|
r->rule_flags |= RULE_IPV6;
|
|
break;
|
|
case 'G':
|
|
r->rule_flags |= RULE_RRQ;
|
|
break;
|
|
case 'P':
|
|
r->rule_flags |= RULE_WRQ;
|
|
break;
|
|
default:
|
|
syslog(LOG_ERR,
|
|
"Remap command \"%s\" on line %d contains invalid char \"%c\"",
|
|
buffer, lineno, *p);
|
|
return -1; /* Error */
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (r->rule_flags & RULE_REWRITE) {
|
|
if (r->rule_flags & RULE_INVERSE) {
|
|
syslog(LOG_ERR, "r rules cannot be inverted, line %d: %s\n",
|
|
lineno, line);
|
|
return -1; /* Error */
|
|
}
|
|
if ((r->rule_flags & (RULE_GLOBAL|RULE_SEDG|RULE_HASFILE))
|
|
== (RULE_GLOBAL|RULE_HASFILE)) {
|
|
syslog(LOG_ERR, "E rules cannot be combined with g (but gg is OK), line %d: %s\n",
|
|
lineno, line);
|
|
return -1; /* Error */
|
|
}
|
|
} else {
|
|
/* RULE_GLOBAL and RULE_SEDG are meaningless without RULE_REWRITE */
|
|
r->rule_flags &= ~(RULE_GLOBAL|RULE_SEDG);
|
|
}
|
|
|
|
/* 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 struct formats *pf,
|
|
const char *input, const struct rule *rules,
|
|
int mode, int af, match_pattern_callback macrosub,
|
|
const char **errmsg)
|
|
{
|
|
char *current = tfstrdup(input);
|
|
char *newstr, *newerstr;
|
|
const char *accerr;
|
|
const struct rule *ruleptr = rules;
|
|
regmatch_t pmatch[10];
|
|
int i;
|
|
int len;
|
|
int was_match = 0;
|
|
int deadman = deadman_max_steps;
|
|
int matchsense;
|
|
int pmatches;
|
|
unsigned int bad_flags;
|
|
int ggoffset;
|
|
|
|
/* Default error */
|
|
*errmsg = "Remap table failure";
|
|
|
|
if (verbosity >= 3) {
|
|
syslog(LOG_INFO, "remap: input: %s", current);
|
|
}
|
|
|
|
bad_flags = 0;
|
|
if (mode != RRQ) bad_flags |= RULE_RRQ;
|
|
if (mode != WRQ) bad_flags |= RULE_WRQ;
|
|
if (af != AF_INET) bad_flags |= RULE_IPV4;
|
|
if (af != AF_INET6) bad_flags |= RULE_IPV6;
|
|
|
|
for (ruleptr = rules; ruleptr; ruleptr = ruleptr->next) {
|
|
if (ruleptr->rule_flags & bad_flags)
|
|
continue; /* This rule is excluded by flags */
|
|
|
|
matchsense = ruleptr->rule_flags & RULE_INVERSE ? REG_NOMATCH : 0;
|
|
pmatches = ruleptr->rule_flags & RULE_INVERSE ? 0 : 10;
|
|
|
|
/* Clear the pmatch[] array */
|
|
for (i = 0; i < 10; i++)
|
|
pmatch[i].rm_so = pmatch[i].rm_eo = -1;
|
|
|
|
was_match = 0;
|
|
|
|
do {
|
|
if (!deadman--)
|
|
goto dead;
|
|
|
|
if (regexec(&ruleptr->rx, current, pmatches, pmatch, 0)
|
|
!= matchsense)
|
|
break; /* No match, break out of do loop */
|
|
|
|
/* Match on this rule */
|
|
was_match = 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 */
|
|
genmatchstring(&newstr, ruleptr->pattern, current,
|
|
pmatch, macrosub, 0, NULL);
|
|
*errmsg = newstr;
|
|
} else {
|
|
*errmsg = NULL;
|
|
}
|
|
free(current);
|
|
return (NULL);
|
|
}
|
|
|
|
if (ruleptr->rule_flags & RULE_REWRITE) {
|
|
len = genmatchstring(&newstr, ruleptr->pattern, current,
|
|
pmatch, macrosub, 0, &ggoffset);
|
|
|
|
if (ruleptr->rule_flags & RULE_SEDG) {
|
|
/* sed-style partial-matching global */
|
|
while (ggoffset < len &&
|
|
regexec(&ruleptr->rx, newstr + ggoffset,
|
|
pmatches, pmatch,
|
|
ggoffset ? REG_NOTBOL : 0)
|
|
== matchsense) {
|
|
if (!deadman--) {
|
|
free(current);
|
|
current = newstr;
|
|
goto dead;
|
|
}
|
|
len = genmatchstring(&newerstr, ruleptr->pattern,
|
|
newstr, pmatch, macrosub,
|
|
ggoffset, &ggoffset);
|
|
free(newstr);
|
|
newstr = newerstr;
|
|
}
|
|
}
|
|
|
|
if ((ruleptr->rule_flags & RULE_HASFILE) &&
|
|
pf->f_validate(newstr, mode, pf, &accerr)) {
|
|
if (verbosity >= 3) {
|
|
syslog(LOG_INFO, "remap: rule %d: ignored rewrite (%s): %s",
|
|
ruleptr->nrule, accerr, newstr);
|
|
}
|
|
free(newstr);
|
|
was_match = 0;
|
|
break;
|
|
}
|
|
|
|
free(current);
|
|
current = newstr;
|
|
if (verbosity >= 3) {
|
|
syslog(LOG_INFO, "remap: rule %d: rewrite: %s",
|
|
ruleptr->nrule, current);
|
|
}
|
|
} else if (ruleptr->rule_flags & RULE_HASFILE) {
|
|
if (pf->f_validate(current, mode, pf, &accerr)) {
|
|
if (verbosity >= 3) {
|
|
syslog(LOG_INFO, "remap: rule %d: not exiting (%s)\n",
|
|
ruleptr->nrule, accerr);
|
|
}
|
|
was_match = 0;
|
|
break;
|
|
}
|
|
}
|
|
/* If the rule is (old-style) global, keep going until no match */
|
|
} while ((ruleptr->rule_flags & (RULE_GLOBAL|RULE_SEDG)) == RULE_GLOBAL);
|
|
|
|
if (!was_match)
|
|
continue; /* Next rule */
|
|
|
|
if (ruleptr->rule_flags & (RULE_EXIT|RULE_HASFILE)) {
|
|
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;
|
|
|
|
dead: /* Deadman expired */
|
|
syslog(LOG_ERR,
|
|
"remap: Breaking loop after %d steps, input = %s, last = %s",
|
|
deadman_max_steps, input, current);
|
|
free(current);
|
|
return NULL; /* Did not terminate! */
|
|
}
|