/* $Id: upnppermissions.c,v 1.24 2025/04/21 22:56:49 nanard Exp $ */
/* MiniUPnP project
 * http://miniupnp.free.fr/ or https://miniupnp.tuxfamily.org/
 * (c) 2006-2023 Thomas Bernard
 * This software is subject to the conditions detailed
 * in the LICENCE file provided within the distribution */

#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <syslog.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#ifdef ENABLE_REGEX
#include <regex.h>
#endif

#include "config.h"
#include "macros.h"
#include "upnppermissions.h"
#include "upnputils.h"

static int
isodigit(char c)
{
	return '0' <= c && c >= '7';
}

static char
hex2chr(char c)
{
	if(c >= 'a')
		return c - 'a';
	if(c >= 'A')
		return c - 'A';
	return c - '0';
}

static char
unescape_char(const char * s, int * seqlen)
{
	char c;
	int len;

	if(s[0] != '\\')
	{
		c = s[0];
		len = 1;
	}
	else
	{
		s++;
		c = s[0];
		len = 2;
		switch(s[0])
		{
		case 'a':  c = '\a'; break;
		case 'b':  c = '\b'; break;
		case 'f':  c = '\f'; break;
		case 'n':  c = '\n'; break;
		case 'r':  c = '\r'; break;
		case 't':  c = '\t'; break;
		case 'v':  c = '\v'; break;
		/* no need: escape the char itself
		case '\\': c = '\\'; break;
		case '\'': c = '\''; break;
		case '"':  c = '"';  break;
		case '?':  c = '?';  break;
		*/
		case 'x':
			if(isxdigit(s[1]) && isxdigit(s[2]))
			{
				c = (hex2chr(s[1]) << 4) + hex2chr(s[2]);
				len = 4;
			}
			break;
		default:
			if(isodigit(s[1]) && isodigit(s[2]) && isodigit(s[3]))
			{
				c = (hex2chr(s[0]) << 6) + (hex2chr(s[1]) << 3) + hex2chr(s[2]);
				len = 4;
			}
		}
	}

	if(seqlen)
		*seqlen = len;
	return c;
}

/* get_next_token(s, &token, raw)
 * put the unquoted/unescaped token in token and returns
 * a pointer to the begining of the next token
 * Do not unescape if raw is true */
static char *
get_next_token(const char * s, char ** token, int raw)
{
	char deli;
	const char * end;

	/* skip any whitespace */
	for(; isspace(*s); s++)
		if(*s == '\0' || *s == '\n')
		{
			if(token)
				*token = NULL;
			return (char *) s;
		}

	/* find the start */
	if(*s == '"' || *s == '\'')
	{
		deli = *s;
		s++;
	}
	else
		deli = 0;
	/* find the end */
	end = s;
	for(; *end != '\0' && *end != '\n' && (deli ? *end != deli : !isspace(*end));
	    end++)
		if(*end == '\\')
		{
			end++;
			if(*end == '\0')
				break;
		}

	/* save the token */
	if(token)
	{
		unsigned int token_len;
		unsigned int i;

		token_len = end - s;
		*token = strndup(s, token_len);
		if(!*token)
			return NULL;

		for(i = 0; (*token)[i] != '\0'; i++)
		{
			int sequence_len;

			if((*token)[i] != '\\')
				continue;

			if(raw && deli && (*token)[i + 1] != deli)
				continue;
			(*token)[i] = unescape_char(*token + i, &sequence_len);
			memmove(*token + i + 1, *token + i + sequence_len,
			        token_len - i - sequence_len);
		}
		if (i == 0)
		{
			/* behavior of realloc(p, 0) is implementation-defined, so better set it to NULL.
			 * https://github.com/miniupnp/miniupnp/issues/652#issuecomment-1518922139 */
			free(*token);
			*token = NULL;
		}
		else
		{
			char * tmp = realloc(*token, i);
			if (tmp != NULL)
				*token = tmp;
			else
				syslog(LOG_ERR, "%s: failed to reallocate to %u bytes",
				       "get_next_token()", i);
		}
	}

	/* return the beginning of the next token */
	if(deli && *end == deli)
		end++;
	while(isspace(*end))
		end++;
	return (char *) end;
}

/* read_permission_line()
 * parse the a permission line which format is :
 * (deny|allow) [0-9]+(-[0-9]+) ip/mask [0-9]+(-[0-9]+) regex
 * ip/mask is either 192.168.1.1/24 or 192.168.1.1/255.255.255.0
 */
int
read_permission_line(struct upnpperm * perm,
                     char * p)
{
	char * q;
	int n_bits;
	int i;

	/* zero memory : see https://github.com/miniupnp/miniupnp/issues/652 */
	memset(perm, 0, sizeof(struct upnpperm));

	/* first token: (allow|deny) */
	while(isspace(*p))
		p++;
	if(0 == memcmp(p, "allow", 5))
	{
		perm->type = UPNPPERM_ALLOW;
		p += 5;
	}
	else if(0 == memcmp(p, "deny", 4))
	{
		perm->type = UPNPPERM_DENY;
		p += 4;
	}
	else
	{
		return -1;
	}
	while(isspace(*p))
		p++;

	/* second token: eport or eport_min-eport_max */
	if(!isdigit(*p))
		return -1;
	for(q = p; isdigit(*q); q++);
	if(*q=='-')
	{
		*q = '\0';
		i = atoi(p);
		if(i > 65535)
			return -1;
		perm->eport_min = (u_short)i;
		q++;
		p = q;
		while(isdigit(*q))
			q++;
		*q = '\0';
		i = atoi(p);
		if(i > 65535)
			return -1;
		perm->eport_max = (u_short)i;
		if(perm->eport_min > perm->eport_max)
			return -1;
	}
	else if(isspace(*q))
	{
		*q = '\0';
		i = atoi(p);
		if(i > 65535)
			return -1;
		perm->eport_min = perm->eport_max = (u_short)i;
	}
	else
	{
		return -1;
	}
	p = q + 1;
	while(isspace(*p))
		p++;

	/* third token:  ip/mask */
	if(!isdigit(*p))
		return -1;
	for(q = p; isdigit(*q) || (*q == '.'); q++);
	if(*q=='/')
	{
		*q = '\0';
		if(!inet_aton(p, &perm->address))
			return -1;
		q++;
		p = q;
		while(isdigit(*q))
			q++;
		if(*q == '.')
		{
			while(*q == '.' || isdigit(*q))
				q++;
			if(!isspace(*q))
				return -1;
			*q = '\0';
			if(!inet_aton(p, &perm->mask))
				return -1;
		}
		else if(!isspace(*q))
			return -1;
		else
		{
			*q = '\0';
			n_bits = atoi(p);
			if(n_bits > 32)
				return -1;
			perm->mask.s_addr = htonl(n_bits ? (0xffffffffu << (32 - n_bits)) : 0);
		}
	}
	else if(isspace(*q))
	{
		*q = '\0';
		if(!inet_aton(p, &perm->address))
			return -1;
		perm->mask.s_addr = 0xffffffffu;
	}
	else
	{
		return -1;
	}
	p = q + 1;

	/* fourth token: iport or iport_min-iport_max */
	while(isspace(*p))
		p++;
	if(!isdigit(*p))
		return -1;
	for(q = p; isdigit(*q); q++);
	if(*q=='-')
	{
		*q = '\0';
		i = atoi(p);
		if(i > 65535)
			return -1;
		perm->iport_min = (u_short)i;
		q++;
		p = q;
		while(isdigit(*q))
			q++;
		*q = '\0';
		i = atoi(p);
		if(i > 65535)
			return -1;
		perm->iport_max = (u_short)i;
		if(perm->iport_min > perm->iport_max)
			return -1;
	}
	else if(isspace(*q) || *q == '\0')
	{
		*q = '\0';
		i = atoi(p);
		if(i > 65535)
			return -1;
		perm->iport_min = perm->iport_max = (u_short)i;
	}
	else
	{
		return -1;
	}
	p = q;

	/* fifth token: (optional) regex */
	p = get_next_token(p, &perm->re, 1);
	if(!p)
	{
		fprintf(stderr, "err when copying regex: out of memory\n");
		return -1;
	}
	if(perm->re)
	{
		if(perm->re[0] == '\0')
		{
			free(perm->re);
			perm->re = NULL;
		}
		else
		{
#ifdef ENABLE_REGEX
			/* icase: if case matters, it must be someone doing something nasty */
			int err;
			err = regcomp(&perm->regex, perm->re,
			              REG_EXTENDED | REG_ICASE | REG_NOSUB);
			if(err)
			{
				char errbuf[256];
				regerror(err, &perm->regex, errbuf, sizeof(errbuf));
				fprintf(stderr, "err when compiling regex \"%s\": %s\n",
				        perm->re, errbuf);
				free(perm->re);
				perm->re = NULL;
				return -1;
			}
#else
			fprintf(stderr, "MiniUPnP is not compiled with ENABLE_REGEX. "
			        "Please remove any regex filter and restart.\n");
			free(perm->re);
			perm->re = NULL;
			return -1;
#endif
		}
	}

#ifdef DEBUG
	printf("perm rule added : %s %hu-%hu %08x/%08x %hu-%hu %s\n",
	       (perm->type==UPNPPERM_ALLOW) ? "allow" : "deny",
	       perm->eport_min, perm->eport_max, ntohl(perm->address.s_addr),
	       ntohl(perm->mask.s_addr), perm->iport_min, perm->iport_max,
	       perm->re ? perm->re : "");
#endif
	return 0;
}

void
free_permission_line(struct upnpperm * perm)
{
	if(perm->re)
	{
		free(perm->re);
		perm->re = NULL;
#ifdef ENABLE_REGEX
		regfree(&perm->regex);
#endif
	}
}

#ifdef USE_MINIUPNPDCTL
void
write_permlist(int fd, const struct upnpperm * permary,
               int nperms)
{
	int l;
	const struct upnpperm * perm;
	int i;
	char buf[128];
	write(fd, "Permissions :\n", 14);
	for(i = 0; i<nperms; i++)
	{
		perm = permary + i;
		l = snprintf(buf, sizeof(buf), "%02d %s %hu-%hu %08x/%08x %hu-%hu",
	       i,
	       (perm->type==UPNPPERM_ALLOW)?"allow":"deny",
	       perm->eport_min, perm->eport_max, ntohl(perm->address.s_addr),
	       ntohl(perm->mask.s_addr), perm->iport_min, perm->iport_max);
		if(l<0)
			return;
		write(fd, buf, l);
		if(perm->re)
		{
			write(fd, " ", 1);
			write(fd, perm->re, strlen(perm->re));
		}
		write(fd, "\n", 1);
	}
}
#endif

/* match_permission()
 * returns: 1 if eport, address, iport matches the permission rule
 *          0 if no match */
static int
match_permission(const struct upnpperm * perm,
                 u_short eport, struct in_addr address, u_short iport,
                 const char * desc)
{
	if( (eport < perm->eport_min) || (perm->eport_max < eport))
		return 0;
	if( (iport < perm->iport_min) || (perm->iport_max < iport))
		return 0;
	if( (address.s_addr & perm->mask.s_addr)
	   != (perm->address.s_addr & perm->mask.s_addr) )
		return 0;
#ifdef ENABLE_REGEX
	if(desc && perm->re && regexec(&perm->regex, desc, 0, NULL, 0) == REG_NOMATCH)
		return 0;
#else
	UNUSED(desc);
#endif
	return 1;
}

#if 0
/* match_permission_internal()
 * returns: 1 if address, iport matches the permission rule
 *          0 if no match */
static int
match_permission_internal(const struct upnpperm * perm,
                          struct in_addr address, u_short iport)
{
	if( (iport < perm->iport_min) || (perm->iport_max < iport))
		return 0;
	if( (address.s_addr & perm->mask.s_addr)
	   != (perm->address.s_addr & perm->mask.s_addr) )
		return 0;
	return 1;
}
#endif

int
check_upnp_rule_against_permissions(const struct upnpperm * permary,
                                    int n_perms,
                                    u_short eport, struct in_addr address,
                                    u_short iport, const char * desc)
{
	int i;
	for(i=0; i<n_perms; i++)
	{
		if(match_permission(permary + i, eport, address, iport, desc))
		{
			syslog(LOG_DEBUG,
			       "UPnP permission rule %d matched : port mapping %s",
			       i, (permary[i].type == UPNPPERM_ALLOW)?"accepted":"rejected"
			       );
			return (permary[i].type == UPNPPERM_ALLOW);
		}
	}
	syslog(LOG_DEBUG, "no permission rule matched : accept by default (n_perms=%d)", n_perms);
	return 1;	/* Default : accept */
}

void
get_permitted_ext_ports(uint32_t * allowed,
                        const struct upnpperm * permary, int n_perms,
                        in_addr_t addr, u_short iport)
{
	int i, j;

	/* build allowed external ports array */
	memset(allowed, 0xff, 65536 / 8);	/* everything allowed by default */

	for (i = n_perms - 1; i >= 0; i--)
	{
		if( (addr & permary[i].mask.s_addr)
		  != (permary[i].address.s_addr & permary[i].mask.s_addr) )
			continue;
		if( (iport < permary[i].iport_min) || (permary[i].iport_max < iport))
			continue;
		for (j = (int)permary[i].eport_min ; j <= (int)permary[i].eport_max; )
		{
			if ((j % 32) == 0 && ((int)permary[i].eport_max >= (j + 31)))
			{
				/* 32bits at once */
				allowed[j / 32] = (permary[i].type == UPNPPERM_ALLOW) ? 0xffffffff : 0;
				j += 32;
			}
			else
			{
				do
				{
					/* one bit at once */
					if (permary[i].type == UPNPPERM_ALLOW)
						allowed[j / 32] |= (1 << (j % 32));
					else
						allowed[j / 32] &= ~(1 << (j % 32));
					j++;
				}
				while ((j % 32) != 0 && (j <= (int)permary[i].eport_max));
			}
		}
	}
}
