mirror of
https://github.com/monero-project/monero.git
synced 2024-12-24 20:49:37 +00:00
a85b5759f3
These files were pulled from the 1.6.3 release tarball. This new version builds against OpenSSL version 1.1 which will be the default in the new Debian Stable which is due to be released RealSoonNow (tm).
1697 lines
45 KiB
C
1697 lines
45 KiB
C
/*
|
|
* testpkts. Data file parse for test packets, and query matching.
|
|
*
|
|
* Data storage for specially crafted replies for testing purposes.
|
|
*
|
|
* (c) NLnet Labs, 2005, 2006, 2007, 2008
|
|
* See the file LICENSE for the license
|
|
*/
|
|
|
|
/**
|
|
* \file
|
|
* This is a debugging aid. It is not efficient, especially
|
|
* with a long config file, but it can give any reply to any query.
|
|
* This can help the developer pre-script replies for queries.
|
|
*
|
|
* You can specify a packet RR by RR with header flags to return.
|
|
*
|
|
* Missing features:
|
|
* - matching content different from reply content.
|
|
* - find way to adjust mangled packets?
|
|
*/
|
|
|
|
#include "config.h"
|
|
struct sockaddr_storage;
|
|
#include <errno.h>
|
|
#include <stdarg.h>
|
|
#include <ctype.h>
|
|
#include "testcode/testpkts.h"
|
|
#include "util/net_help.h"
|
|
#include "sldns/sbuffer.h"
|
|
#include "sldns/rrdef.h"
|
|
#include "sldns/pkthdr.h"
|
|
#include "sldns/str2wire.h"
|
|
#include "sldns/wire2str.h"
|
|
|
|
/** max size of a packet */
|
|
#define MAX_PACKETLEN 65536
|
|
/** max line length */
|
|
#define MAX_LINE 10240
|
|
/** string to show in warnings and errors */
|
|
static const char* prog_name = "testpkts";
|
|
|
|
#ifndef UTIL_LOG_H
|
|
/** verbosity definition for compat */
|
|
enum verbosity_value { NO_VERBOSE=0 };
|
|
#endif
|
|
/** logging routine, provided by caller */
|
|
void verbose(enum verbosity_value lvl, const char* msg, ...) ATTR_FORMAT(printf, 2, 3);
|
|
|
|
/** print error and exit */
|
|
static void error(const char* msg, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, msg);
|
|
fprintf(stderr, "%s error: ", prog_name);
|
|
vfprintf(stderr, msg, args);
|
|
fprintf(stderr, "\n");
|
|
fflush(stderr);
|
|
va_end(args);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/** return if string is empty or comment */
|
|
static int isendline(char c)
|
|
{
|
|
if(c == ';' || c == '#'
|
|
|| c == '\n' || c == 0)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
/** true if the string starts with the keyword given. Moves the str ahead.
|
|
* @param str: before keyword, afterwards after keyword and spaces.
|
|
* @param keyword: the keyword to match
|
|
* @return: true if keyword present. False otherwise, and str unchanged.
|
|
*/
|
|
static int str_keyword(char** str, const char* keyword)
|
|
{
|
|
size_t len = strlen(keyword);
|
|
assert(str && keyword);
|
|
if(strncmp(*str, keyword, len) != 0)
|
|
return 0;
|
|
*str += len;
|
|
while(isspace((unsigned char)**str))
|
|
(*str)++;
|
|
return 1;
|
|
}
|
|
|
|
/** Add reply packet to entry */
|
|
static struct reply_packet*
|
|
entry_add_reply(struct entry* entry)
|
|
{
|
|
struct reply_packet* pkt = (struct reply_packet*)malloc(
|
|
sizeof(struct reply_packet));
|
|
struct reply_packet ** p = &entry->reply_list;
|
|
if(!pkt) error("out of memory");
|
|
pkt->next = NULL;
|
|
pkt->packet_sleep = 0;
|
|
pkt->reply_pkt = NULL;
|
|
pkt->reply_from_hex = NULL;
|
|
pkt->raw_ednsdata = NULL;
|
|
/* link at end */
|
|
while(*p)
|
|
p = &((*p)->next);
|
|
*p = pkt;
|
|
return pkt;
|
|
}
|
|
|
|
/** parse MATCH line */
|
|
static void matchline(char* line, struct entry* e)
|
|
{
|
|
char* parse = line;
|
|
while(*parse) {
|
|
if(isendline(*parse))
|
|
return;
|
|
if(str_keyword(&parse, "opcode")) {
|
|
e->match_opcode = 1;
|
|
} else if(str_keyword(&parse, "qtype")) {
|
|
e->match_qtype = 1;
|
|
} else if(str_keyword(&parse, "qname")) {
|
|
e->match_qname = 1;
|
|
} else if(str_keyword(&parse, "rcode")) {
|
|
e->match_rcode = 1;
|
|
} else if(str_keyword(&parse, "question")) {
|
|
e->match_question = 1;
|
|
} else if(str_keyword(&parse, "answer")) {
|
|
e->match_answer = 1;
|
|
} else if(str_keyword(&parse, "subdomain")) {
|
|
e->match_subdomain = 1;
|
|
} else if(str_keyword(&parse, "all")) {
|
|
e->match_all = 1;
|
|
} else if(str_keyword(&parse, "ttl")) {
|
|
e->match_ttl = 1;
|
|
} else if(str_keyword(&parse, "DO")) {
|
|
e->match_do = 1;
|
|
} else if(str_keyword(&parse, "noedns")) {
|
|
e->match_noedns = 1;
|
|
} else if(str_keyword(&parse, "ednsdata")) {
|
|
e->match_ednsdata_raw = 1;
|
|
} else if(str_keyword(&parse, "UDP")) {
|
|
e->match_transport = transport_udp;
|
|
} else if(str_keyword(&parse, "TCP")) {
|
|
e->match_transport = transport_tcp;
|
|
} else if(str_keyword(&parse, "serial")) {
|
|
e->match_serial = 1;
|
|
if(*parse != '=' && *parse != ':')
|
|
error("expected = or : in MATCH: %s", line);
|
|
parse++;
|
|
e->ixfr_soa_serial = (uint32_t)strtol(parse, (char**)&parse, 10);
|
|
while(isspace((unsigned char)*parse))
|
|
parse++;
|
|
} else {
|
|
error("could not parse MATCH: '%s'", parse);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** parse REPLY line */
|
|
static void replyline(char* line, uint8_t* reply, size_t reply_len,
|
|
int* do_flag)
|
|
{
|
|
char* parse = line;
|
|
if(reply_len < LDNS_HEADER_SIZE) error("packet too short for header");
|
|
while(*parse) {
|
|
if(isendline(*parse))
|
|
return;
|
|
/* opcodes */
|
|
if(str_keyword(&parse, "QUERY")) {
|
|
LDNS_OPCODE_SET(reply, LDNS_PACKET_QUERY);
|
|
} else if(str_keyword(&parse, "IQUERY")) {
|
|
LDNS_OPCODE_SET(reply, LDNS_PACKET_IQUERY);
|
|
} else if(str_keyword(&parse, "STATUS")) {
|
|
LDNS_OPCODE_SET(reply, LDNS_PACKET_STATUS);
|
|
} else if(str_keyword(&parse, "NOTIFY")) {
|
|
LDNS_OPCODE_SET(reply, LDNS_PACKET_NOTIFY);
|
|
} else if(str_keyword(&parse, "UPDATE")) {
|
|
LDNS_OPCODE_SET(reply, LDNS_PACKET_UPDATE);
|
|
/* rcodes */
|
|
} else if(str_keyword(&parse, "NOERROR")) {
|
|
LDNS_RCODE_SET(reply, LDNS_RCODE_NOERROR);
|
|
} else if(str_keyword(&parse, "FORMERR")) {
|
|
LDNS_RCODE_SET(reply, LDNS_RCODE_FORMERR);
|
|
} else if(str_keyword(&parse, "SERVFAIL")) {
|
|
LDNS_RCODE_SET(reply, LDNS_RCODE_SERVFAIL);
|
|
} else if(str_keyword(&parse, "NXDOMAIN")) {
|
|
LDNS_RCODE_SET(reply, LDNS_RCODE_NXDOMAIN);
|
|
} else if(str_keyword(&parse, "NOTIMPL")) {
|
|
LDNS_RCODE_SET(reply, LDNS_RCODE_NOTIMPL);
|
|
} else if(str_keyword(&parse, "REFUSED")) {
|
|
LDNS_RCODE_SET(reply, LDNS_RCODE_REFUSED);
|
|
} else if(str_keyword(&parse, "YXDOMAIN")) {
|
|
LDNS_RCODE_SET(reply, LDNS_RCODE_YXDOMAIN);
|
|
} else if(str_keyword(&parse, "YXRRSET")) {
|
|
LDNS_RCODE_SET(reply, LDNS_RCODE_YXRRSET);
|
|
} else if(str_keyword(&parse, "NXRRSET")) {
|
|
LDNS_RCODE_SET(reply, LDNS_RCODE_NXRRSET);
|
|
} else if(str_keyword(&parse, "NOTAUTH")) {
|
|
LDNS_RCODE_SET(reply, LDNS_RCODE_NOTAUTH);
|
|
} else if(str_keyword(&parse, "NOTZONE")) {
|
|
LDNS_RCODE_SET(reply, LDNS_RCODE_NOTZONE);
|
|
/* flags */
|
|
} else if(str_keyword(&parse, "QR")) {
|
|
LDNS_QR_SET(reply);
|
|
} else if(str_keyword(&parse, "AA")) {
|
|
LDNS_AA_SET(reply);
|
|
} else if(str_keyword(&parse, "TC")) {
|
|
LDNS_TC_SET(reply);
|
|
} else if(str_keyword(&parse, "RD")) {
|
|
LDNS_RD_SET(reply);
|
|
} else if(str_keyword(&parse, "CD")) {
|
|
LDNS_CD_SET(reply);
|
|
} else if(str_keyword(&parse, "RA")) {
|
|
LDNS_RA_SET(reply);
|
|
} else if(str_keyword(&parse, "AD")) {
|
|
LDNS_AD_SET(reply);
|
|
} else if(str_keyword(&parse, "DO")) {
|
|
*do_flag = 1;
|
|
} else {
|
|
error("could not parse REPLY: '%s'", parse);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** parse ADJUST line */
|
|
static void adjustline(char* line, struct entry* e,
|
|
struct reply_packet* pkt)
|
|
{
|
|
char* parse = line;
|
|
while(*parse) {
|
|
if(isendline(*parse))
|
|
return;
|
|
if(str_keyword(&parse, "copy_id")) {
|
|
e->copy_id = 1;
|
|
} else if(str_keyword(&parse, "copy_query")) {
|
|
e->copy_query = 1;
|
|
} else if(str_keyword(&parse, "copy_ednsdata_assume_clientsubnet")) {
|
|
e->copy_ednsdata_assume_clientsubnet = 1;
|
|
} else if(str_keyword(&parse, "sleep=")) {
|
|
e->sleeptime = (unsigned int) strtol(parse, (char**)&parse, 10);
|
|
while(isspace((unsigned char)*parse))
|
|
parse++;
|
|
} else if(str_keyword(&parse, "packet_sleep=")) {
|
|
pkt->packet_sleep = (unsigned int) strtol(parse, (char**)&parse, 10);
|
|
while(isspace((unsigned char)*parse))
|
|
parse++;
|
|
} else {
|
|
error("could not parse ADJUST: '%s'", parse);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** create new entry */
|
|
static struct entry* new_entry(void)
|
|
{
|
|
struct entry* e = (struct entry*)malloc(sizeof(struct entry));
|
|
if(!e) error("out of memory");
|
|
memset(e, 0, sizeof(*e));
|
|
e->match_opcode = 0;
|
|
e->match_qtype = 0;
|
|
e->match_qname = 0;
|
|
e->match_rcode = 0;
|
|
e->match_question = 0;
|
|
e->match_answer = 0;
|
|
e->match_subdomain = 0;
|
|
e->match_all = 0;
|
|
e->match_ttl = 0;
|
|
e->match_do = 0;
|
|
e->match_noedns = 0;
|
|
e->match_serial = 0;
|
|
e->ixfr_soa_serial = 0;
|
|
e->match_transport = transport_any;
|
|
e->reply_list = NULL;
|
|
e->copy_id = 0;
|
|
e->copy_query = 0;
|
|
e->copy_ednsdata_assume_clientsubnet = 0;
|
|
e->sleeptime = 0;
|
|
e->next = NULL;
|
|
return e;
|
|
}
|
|
|
|
/**
|
|
* Converts a hex string to binary data
|
|
* @param hexstr: string of hex.
|
|
* @param len: is the length of the string
|
|
* @param buf: is the buffer to store the result in
|
|
* @param offset: is the starting position in the result buffer
|
|
* @param buf_len: is the length of buf.
|
|
* @return This function returns the length of the result
|
|
*/
|
|
static size_t
|
|
hexstr2bin(char *hexstr, int len, uint8_t *buf, size_t offset, size_t buf_len)
|
|
{
|
|
char c;
|
|
int i;
|
|
uint8_t int8 = 0;
|
|
int sec = 0;
|
|
size_t bufpos = 0;
|
|
|
|
if (len % 2 != 0) {
|
|
return 0;
|
|
}
|
|
|
|
for (i=0; i<len; i++) {
|
|
c = hexstr[i];
|
|
|
|
/* case insensitive, skip spaces */
|
|
if (c != ' ') {
|
|
if (c >= '0' && c <= '9') {
|
|
int8 += c & 0x0f;
|
|
} else if (c >= 'a' && c <= 'z') {
|
|
int8 += (c & 0x0f) + 9;
|
|
} else if (c >= 'A' && c <= 'Z') {
|
|
int8 += (c & 0x0f) + 9;
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
if (sec == 0) {
|
|
int8 = int8 << 4;
|
|
sec = 1;
|
|
} else {
|
|
if (bufpos + offset + 1 <= buf_len) {
|
|
buf[bufpos+offset] = int8;
|
|
int8 = 0;
|
|
sec = 0;
|
|
bufpos++;
|
|
} else {
|
|
fprintf(stderr, "Buffer too small in hexstr2bin");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return bufpos;
|
|
}
|
|
|
|
/** convert hex buffer to binary buffer */
|
|
static sldns_buffer *
|
|
hex_buffer2wire(sldns_buffer *data_buffer)
|
|
{
|
|
sldns_buffer *wire_buffer = NULL;
|
|
int c;
|
|
|
|
/* stat hack
|
|
* 0 = normal
|
|
* 1 = comment (skip to end of line)
|
|
* 2 = unprintable character found, read binary data directly
|
|
*/
|
|
size_t data_buf_pos = 0;
|
|
int state = 0;
|
|
uint8_t *hexbuf;
|
|
int hexbufpos = 0;
|
|
size_t wirelen;
|
|
uint8_t *data_wire = (uint8_t *) sldns_buffer_begin(data_buffer);
|
|
uint8_t *wire = (uint8_t*)malloc(MAX_PACKETLEN);
|
|
if(!wire) error("out of memory");
|
|
|
|
hexbuf = (uint8_t*)malloc(MAX_PACKETLEN);
|
|
if(!hexbuf) error("out of memory");
|
|
for (data_buf_pos = 0; data_buf_pos < sldns_buffer_position(data_buffer); data_buf_pos++) {
|
|
c = (int) data_wire[data_buf_pos];
|
|
|
|
if (state < 2 && !isascii((unsigned char)c)) {
|
|
/*verbose("non ascii character found in file: (%d) switching to raw mode\n", c);*/
|
|
state = 2;
|
|
}
|
|
switch (state) {
|
|
case 0:
|
|
if ( (c >= '0' && c <= '9') ||
|
|
(c >= 'a' && c <= 'f') ||
|
|
(c >= 'A' && c <= 'F') )
|
|
{
|
|
if (hexbufpos >= MAX_PACKETLEN) {
|
|
error("buffer overflow");
|
|
free(hexbuf);
|
|
return 0;
|
|
|
|
}
|
|
hexbuf[hexbufpos] = (uint8_t) c;
|
|
hexbufpos++;
|
|
} else if (c == ';') {
|
|
state = 1;
|
|
} else if (c == ' ' || c == '\t' || c == '\n') {
|
|
/* skip whitespace */
|
|
}
|
|
break;
|
|
case 1:
|
|
if (c == '\n' || c == EOF) {
|
|
state = 0;
|
|
}
|
|
break;
|
|
case 2:
|
|
if (hexbufpos >= MAX_PACKETLEN) {
|
|
error("buffer overflow");
|
|
free(hexbuf);
|
|
return 0;
|
|
}
|
|
hexbuf[hexbufpos] = (uint8_t) c;
|
|
hexbufpos++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hexbufpos >= MAX_PACKETLEN) {
|
|
/*verbose("packet size reached\n");*/
|
|
}
|
|
|
|
/* lenient mode: length must be multiple of 2 */
|
|
if (hexbufpos % 2 != 0) {
|
|
if (hexbufpos >= MAX_PACKETLEN) {
|
|
error("buffer overflow");
|
|
free(hexbuf);
|
|
return 0;
|
|
}
|
|
hexbuf[hexbufpos] = (uint8_t) '0';
|
|
hexbufpos++;
|
|
}
|
|
|
|
if (state < 2) {
|
|
wirelen = hexstr2bin((char *) hexbuf, hexbufpos, wire, 0, MAX_PACKETLEN);
|
|
wire_buffer = sldns_buffer_new(wirelen);
|
|
sldns_buffer_new_frm_data(wire_buffer, wire, wirelen);
|
|
} else {
|
|
error("Incomplete hex data, not at byte boundary\n");
|
|
}
|
|
free(wire);
|
|
free(hexbuf);
|
|
return wire_buffer;
|
|
}
|
|
|
|
/** parse ORIGIN */
|
|
static void
|
|
get_origin(const char* name, struct sldns_file_parse_state* pstate, char* parse)
|
|
{
|
|
/* snip off rest of the text so as to make the parse work in ldns */
|
|
char* end;
|
|
char store;
|
|
int status;
|
|
|
|
end=parse;
|
|
while(!isspace((unsigned char)*end) && !isendline(*end))
|
|
end++;
|
|
store = *end;
|
|
*end = 0;
|
|
verbose(3, "parsing '%s'\n", parse);
|
|
status = sldns_str2wire_dname_buf(parse, pstate->origin,
|
|
&pstate->origin_len);
|
|
*end = store;
|
|
if(status != 0)
|
|
error("%s line %d:\n\t%s: %s", name, pstate->lineno,
|
|
sldns_get_errorstr_parse(status), parse);
|
|
}
|
|
|
|
/** add RR to packet */
|
|
static void add_rr(char* rrstr, uint8_t* pktbuf, size_t pktsize,
|
|
size_t* pktlen, struct sldns_file_parse_state* pstate,
|
|
sldns_pkt_section add_section, const char* fname)
|
|
{
|
|
/* it must be a RR, parse and add to packet. */
|
|
size_t rr_len = pktsize - *pktlen;
|
|
size_t dname_len = 0;
|
|
int status;
|
|
uint8_t* origin = pstate->origin_len?pstate->origin:0;
|
|
uint8_t* prev = pstate->prev_rr_len?pstate->prev_rr:0;
|
|
if(*pktlen > pktsize || *pktlen < LDNS_HEADER_SIZE)
|
|
error("packet overflow");
|
|
|
|
/* parse RR */
|
|
if(add_section == LDNS_SECTION_QUESTION)
|
|
status = sldns_str2wire_rr_question_buf(rrstr, pktbuf+*pktlen,
|
|
&rr_len, &dname_len, origin, pstate->origin_len,
|
|
prev, pstate->prev_rr_len);
|
|
else status = sldns_str2wire_rr_buf(rrstr, pktbuf+*pktlen, &rr_len,
|
|
&dname_len, pstate->default_ttl, origin,
|
|
pstate->origin_len, prev, pstate->prev_rr_len);
|
|
if(status != 0)
|
|
error("%s line %d:%d %s\n\t%s", fname, pstate->lineno,
|
|
LDNS_WIREPARSE_OFFSET(status),
|
|
sldns_get_errorstr_parse(status), rrstr);
|
|
*pktlen += rr_len;
|
|
|
|
/* increase RR count */
|
|
if(add_section == LDNS_SECTION_QUESTION)
|
|
sldns_write_uint16(pktbuf+4, LDNS_QDCOUNT(pktbuf)+1);
|
|
else if(add_section == LDNS_SECTION_ANSWER)
|
|
sldns_write_uint16(pktbuf+6, LDNS_ANCOUNT(pktbuf)+1);
|
|
else if(add_section == LDNS_SECTION_AUTHORITY)
|
|
sldns_write_uint16(pktbuf+8, LDNS_NSCOUNT(pktbuf)+1);
|
|
else if(add_section == LDNS_SECTION_ADDITIONAL)
|
|
sldns_write_uint16(pktbuf+10, LDNS_ARCOUNT(pktbuf)+1);
|
|
else error("internal error bad section %d", (int)add_section);
|
|
}
|
|
|
|
/* add EDNS 4096 opt record */
|
|
static void
|
|
add_edns(uint8_t* pktbuf, size_t pktsize, int do_flag, uint8_t *ednsdata,
|
|
uint16_t ednslen, size_t* pktlen)
|
|
{
|
|
uint8_t edns[] = {0x00, /* root label */
|
|
0x00, LDNS_RR_TYPE_OPT, /* type */
|
|
0x10, 0x00, /* class is UDPSIZE 4096 */
|
|
0x00, /* TTL[0] is ext rcode */
|
|
0x00, /* TTL[1] is edns version */
|
|
(uint8_t)(do_flag?0x80:0x00), 0x00, /* TTL[2-3] is edns flags, DO */
|
|
(uint8_t)((ednslen >> 8) & 0xff),
|
|
(uint8_t)(ednslen & 0xff), /* rdatalength */
|
|
};
|
|
if(*pktlen < LDNS_HEADER_SIZE)
|
|
return;
|
|
if(*pktlen + sizeof(edns) + ednslen > pktsize)
|
|
error("not enough space for EDNS OPT record");
|
|
memmove(pktbuf+*pktlen, edns, sizeof(edns));
|
|
memmove(pktbuf+*pktlen+sizeof(edns), ednsdata, ednslen);
|
|
sldns_write_uint16(pktbuf+10, LDNS_ARCOUNT(pktbuf)+1);
|
|
*pktlen += (sizeof(edns) + ednslen);
|
|
}
|
|
|
|
/* Reads one entry from file. Returns entry or NULL on error. */
|
|
struct entry*
|
|
read_entry(FILE* in, const char* name, struct sldns_file_parse_state* pstate,
|
|
int skip_whitespace)
|
|
{
|
|
struct entry* current = NULL;
|
|
char line[MAX_LINE];
|
|
char* parse;
|
|
sldns_pkt_section add_section = LDNS_SECTION_QUESTION;
|
|
struct reply_packet *cur_reply = NULL;
|
|
int reading_hex = 0;
|
|
int reading_hex_ednsdata = 0;
|
|
sldns_buffer* hex_data_buffer = NULL;
|
|
sldns_buffer* hex_ednsdata_buffer = NULL;
|
|
uint8_t pktbuf[MAX_PACKETLEN];
|
|
size_t pktlen = LDNS_HEADER_SIZE;
|
|
int do_flag = 0; /* DO flag in EDNS */
|
|
memset(pktbuf, 0, pktlen); /* ID = 0, FLAGS="", and rr counts 0 */
|
|
|
|
while(fgets(line, (int)sizeof(line), in) != NULL) {
|
|
line[MAX_LINE-1] = 0;
|
|
parse = line;
|
|
pstate->lineno++;
|
|
|
|
while(isspace((unsigned char)*parse))
|
|
parse++;
|
|
/* test for keywords */
|
|
if(isendline(*parse))
|
|
continue; /* skip comment and empty lines */
|
|
if(str_keyword(&parse, "ENTRY_BEGIN")) {
|
|
if(current) {
|
|
error("%s line %d: previous entry does not ENTRY_END",
|
|
name, pstate->lineno);
|
|
}
|
|
current = new_entry();
|
|
current->lineno = pstate->lineno;
|
|
cur_reply = entry_add_reply(current);
|
|
continue;
|
|
} else if(str_keyword(&parse, "$ORIGIN")) {
|
|
get_origin(name, pstate, parse);
|
|
continue;
|
|
} else if(str_keyword(&parse, "$TTL")) {
|
|
pstate->default_ttl = (uint32_t)atoi(parse);
|
|
continue;
|
|
}
|
|
|
|
/* working inside an entry */
|
|
if(!current) {
|
|
error("%s line %d: expected ENTRY_BEGIN but got %s",
|
|
name, pstate->lineno, line);
|
|
}
|
|
if(str_keyword(&parse, "MATCH")) {
|
|
matchline(parse, current);
|
|
} else if(str_keyword(&parse, "REPLY")) {
|
|
replyline(parse, pktbuf, pktlen, &do_flag);
|
|
} else if(str_keyword(&parse, "ADJUST")) {
|
|
adjustline(parse, current, cur_reply);
|
|
} else if(str_keyword(&parse, "EXTRA_PACKET")) {
|
|
cur_reply = entry_add_reply(current);
|
|
} else if(str_keyword(&parse, "SECTION")) {
|
|
if(str_keyword(&parse, "QUESTION"))
|
|
add_section = LDNS_SECTION_QUESTION;
|
|
else if(str_keyword(&parse, "ANSWER"))
|
|
add_section = LDNS_SECTION_ANSWER;
|
|
else if(str_keyword(&parse, "AUTHORITY"))
|
|
add_section = LDNS_SECTION_AUTHORITY;
|
|
else if(str_keyword(&parse, "ADDITIONAL"))
|
|
add_section = LDNS_SECTION_ADDITIONAL;
|
|
else error("%s line %d: bad section %s", name, pstate->lineno, parse);
|
|
} else if(str_keyword(&parse, "HEX_ANSWER_BEGIN")) {
|
|
hex_data_buffer = sldns_buffer_new(MAX_PACKETLEN);
|
|
reading_hex = 1;
|
|
} else if(str_keyword(&parse, "HEX_ANSWER_END")) {
|
|
if(!reading_hex) {
|
|
error("%s line %d: HEX_ANSWER_END read but no HEX_ANSWER_BEGIN keyword seen", name, pstate->lineno);
|
|
}
|
|
reading_hex = 0;
|
|
cur_reply->reply_from_hex = hex_buffer2wire(hex_data_buffer);
|
|
sldns_buffer_free(hex_data_buffer);
|
|
hex_data_buffer = NULL;
|
|
} else if(reading_hex) {
|
|
sldns_buffer_printf(hex_data_buffer, "%s", line);
|
|
} else if(str_keyword(&parse, "HEX_EDNSDATA_BEGIN")) {
|
|
hex_ednsdata_buffer = sldns_buffer_new(MAX_PACKETLEN);
|
|
reading_hex_ednsdata = 1;
|
|
} else if(str_keyword(&parse, "HEX_EDNSDATA_END")) {
|
|
if (!reading_hex_ednsdata) {
|
|
error("%s line %d: HEX_EDNSDATA_END read but no"
|
|
"HEX_EDNSDATA_BEGIN keyword seen", name, pstate->lineno);
|
|
}
|
|
reading_hex_ednsdata = 0;
|
|
cur_reply->raw_ednsdata = hex_buffer2wire(hex_ednsdata_buffer);
|
|
sldns_buffer_free(hex_ednsdata_buffer);
|
|
hex_ednsdata_buffer = NULL;
|
|
} else if(reading_hex_ednsdata) {
|
|
sldns_buffer_printf(hex_ednsdata_buffer, "%s", line);
|
|
} else if(str_keyword(&parse, "ENTRY_END")) {
|
|
if(hex_data_buffer)
|
|
sldns_buffer_free(hex_data_buffer);
|
|
if(hex_ednsdata_buffer)
|
|
sldns_buffer_free(hex_ednsdata_buffer);
|
|
if(pktlen != 0) {
|
|
if(do_flag || cur_reply->raw_ednsdata) {
|
|
if(cur_reply->raw_ednsdata &&
|
|
sldns_buffer_limit(cur_reply->raw_ednsdata))
|
|
add_edns(pktbuf, sizeof(pktbuf), do_flag,
|
|
sldns_buffer_begin(cur_reply->raw_ednsdata),
|
|
(uint16_t)sldns_buffer_limit(cur_reply->raw_ednsdata),
|
|
&pktlen);
|
|
else
|
|
add_edns(pktbuf, sizeof(pktbuf), do_flag,
|
|
NULL, 0, &pktlen);
|
|
}
|
|
cur_reply->reply_pkt = memdup(pktbuf, pktlen);
|
|
cur_reply->reply_len = pktlen;
|
|
if(!cur_reply->reply_pkt)
|
|
error("out of memory");
|
|
}
|
|
return current;
|
|
} else {
|
|
add_rr(skip_whitespace?parse:line, pktbuf,
|
|
sizeof(pktbuf), &pktlen, pstate, add_section,
|
|
name);
|
|
}
|
|
|
|
}
|
|
if(reading_hex) {
|
|
error("%s: End of file reached while still reading hex, "
|
|
"missing HEX_ANSWER_END\n", name);
|
|
}
|
|
if(reading_hex_ednsdata) {
|
|
error("%s: End of file reached while still reading edns data, "
|
|
"missing HEX_EDNSDATA_END\n", name);
|
|
}
|
|
if(current) {
|
|
error("%s: End of file reached while reading entry. "
|
|
"missing ENTRY_END\n", name);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* reads the canned reply file and returns a list of structs */
|
|
struct entry*
|
|
read_datafile(const char* name, int skip_whitespace)
|
|
{
|
|
struct entry* list = NULL;
|
|
struct entry* last = NULL;
|
|
struct entry* current = NULL;
|
|
FILE *in;
|
|
struct sldns_file_parse_state pstate;
|
|
int entry_num = 0;
|
|
memset(&pstate, 0, sizeof(pstate));
|
|
|
|
if((in=fopen(name, "r")) == NULL) {
|
|
error("could not open file %s: %s", name, strerror(errno));
|
|
}
|
|
|
|
while((current = read_entry(in, name, &pstate, skip_whitespace)))
|
|
{
|
|
if(last)
|
|
last->next = current;
|
|
else list = current;
|
|
last = current;
|
|
entry_num ++;
|
|
}
|
|
verbose(1, "%s: Read %d entries\n", prog_name, entry_num);
|
|
|
|
fclose(in);
|
|
return list;
|
|
}
|
|
|
|
/** get qtype from packet */
|
|
static sldns_rr_type get_qtype(uint8_t* pkt, size_t pktlen)
|
|
{
|
|
uint8_t* d;
|
|
size_t dl, sl=0;
|
|
char* snull = NULL;
|
|
if(pktlen < LDNS_HEADER_SIZE)
|
|
return 0;
|
|
if(LDNS_QDCOUNT(pkt) == 0)
|
|
return 0;
|
|
/* skip over dname with dname-scan routine */
|
|
d = pkt+LDNS_HEADER_SIZE;
|
|
dl = pktlen-LDNS_HEADER_SIZE;
|
|
(void)sldns_wire2str_dname_scan(&d, &dl, &snull, &sl, pkt, pktlen);
|
|
if(dl < 2)
|
|
return 0;
|
|
return sldns_read_uint16(d);
|
|
}
|
|
|
|
/** get qtype from packet */
|
|
static size_t get_qname_len(uint8_t* pkt, size_t pktlen)
|
|
{
|
|
uint8_t* d;
|
|
size_t dl, sl=0;
|
|
char* snull = NULL;
|
|
if(pktlen < LDNS_HEADER_SIZE)
|
|
return 0;
|
|
if(LDNS_QDCOUNT(pkt) == 0)
|
|
return 0;
|
|
/* skip over dname with dname-scan routine */
|
|
d = pkt+LDNS_HEADER_SIZE;
|
|
dl = pktlen-LDNS_HEADER_SIZE;
|
|
(void)sldns_wire2str_dname_scan(&d, &dl, &snull, &sl, pkt, pktlen);
|
|
return pktlen-dl-LDNS_HEADER_SIZE;
|
|
}
|
|
|
|
/** returns owner from packet */
|
|
static uint8_t* get_qname(uint8_t* pkt, size_t pktlen)
|
|
{
|
|
if(pktlen < LDNS_HEADER_SIZE)
|
|
return NULL;
|
|
if(LDNS_QDCOUNT(pkt) == 0)
|
|
return NULL;
|
|
return pkt+LDNS_HEADER_SIZE;
|
|
}
|
|
|
|
/** returns opcode from packet */
|
|
static int get_opcode(uint8_t* pkt, size_t pktlen)
|
|
{
|
|
if(pktlen < LDNS_HEADER_SIZE)
|
|
return 0;
|
|
return (int)LDNS_OPCODE_WIRE(pkt);
|
|
}
|
|
|
|
/** returns rcode from packet */
|
|
static int get_rcode(uint8_t* pkt, size_t pktlen)
|
|
{
|
|
if(pktlen < LDNS_HEADER_SIZE)
|
|
return 0;
|
|
return (int)LDNS_RCODE_WIRE(pkt);
|
|
}
|
|
|
|
/** get authority section SOA serial value */
|
|
static uint32_t get_serial(uint8_t* p, size_t plen)
|
|
{
|
|
uint8_t* walk = p;
|
|
size_t walk_len = plen, sl=0;
|
|
char* snull = NULL;
|
|
uint16_t i;
|
|
|
|
if(walk_len < LDNS_HEADER_SIZE)
|
|
return 0;
|
|
walk += LDNS_HEADER_SIZE;
|
|
walk_len -= LDNS_HEADER_SIZE;
|
|
|
|
/* skip other records with wire2str_scan */
|
|
for(i=0; i < LDNS_QDCOUNT(p); i++)
|
|
(void)sldns_wire2str_rrquestion_scan(&walk, &walk_len,
|
|
&snull, &sl, p, plen);
|
|
for(i=0; i < LDNS_ANCOUNT(p); i++)
|
|
(void)sldns_wire2str_rr_scan(&walk, &walk_len, &snull, &sl,
|
|
p, plen);
|
|
|
|
/* walk through authority section */
|
|
for(i=0; i < LDNS_NSCOUNT(p); i++) {
|
|
/* if this is SOA then get serial, skip compressed dname */
|
|
uint8_t* dstart = walk;
|
|
size_t dlen = walk_len;
|
|
(void)sldns_wire2str_dname_scan(&dstart, &dlen, &snull, &sl,
|
|
p, plen);
|
|
if(dlen >= 2 && sldns_read_uint16(dstart) == LDNS_RR_TYPE_SOA) {
|
|
/* skip type, class, TTL, rdatalen */
|
|
if(dlen < 10)
|
|
return 0;
|
|
if(dlen < 10 + (size_t)sldns_read_uint16(dstart+8))
|
|
return 0;
|
|
dstart += 10;
|
|
dlen -= 10;
|
|
/* check third rdf */
|
|
(void)sldns_wire2str_dname_scan(&dstart, &dlen, &snull,
|
|
&sl, p, plen);
|
|
(void)sldns_wire2str_dname_scan(&dstart, &dlen, &snull,
|
|
&sl, p, plen);
|
|
if(dlen < 4)
|
|
return 0;
|
|
verbose(3, "found serial %u in msg. ",
|
|
(int)sldns_read_uint32(dstart));
|
|
return sldns_read_uint32(dstart);
|
|
}
|
|
/* move to next RR */
|
|
(void)sldns_wire2str_rr_scan(&walk, &walk_len, &snull, &sl,
|
|
p, plen);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/** get ptr to EDNS OPT record (and remaining length); behind the type u16 */
|
|
static int
|
|
pkt_find_edns_opt(uint8_t** p, size_t* plen)
|
|
{
|
|
/* walk over the packet with scan routines */
|
|
uint8_t* w = *p;
|
|
size_t wlen = *plen, sl=0;
|
|
char* snull = NULL;
|
|
uint16_t i;
|
|
|
|
if(wlen < LDNS_HEADER_SIZE)
|
|
return 0;
|
|
w += LDNS_HEADER_SIZE;
|
|
wlen -= LDNS_HEADER_SIZE;
|
|
|
|
/* skip other records with wire2str_scan */
|
|
for(i=0; i < LDNS_QDCOUNT(*p); i++)
|
|
(void)sldns_wire2str_rrquestion_scan(&w, &wlen, &snull, &sl,
|
|
*p, *plen);
|
|
for(i=0; i < LDNS_ANCOUNT(*p); i++)
|
|
(void)sldns_wire2str_rr_scan(&w, &wlen, &snull, &sl, *p, *plen);
|
|
for(i=0; i < LDNS_NSCOUNT(*p); i++)
|
|
(void)sldns_wire2str_rr_scan(&w, &wlen, &snull, &sl, *p, *plen);
|
|
|
|
/* walk through additional section */
|
|
for(i=0; i < LDNS_ARCOUNT(*p); i++) {
|
|
/* if this is OPT then done */
|
|
uint8_t* dstart = w;
|
|
size_t dlen = wlen;
|
|
(void)sldns_wire2str_dname_scan(&dstart, &dlen, &snull, &sl,
|
|
*p, *plen);
|
|
if(dlen >= 2 && sldns_read_uint16(dstart) == LDNS_RR_TYPE_OPT) {
|
|
*p = dstart+2;
|
|
*plen = dlen-2;
|
|
return 1;
|
|
}
|
|
/* move to next RR */
|
|
(void)sldns_wire2str_rr_scan(&w, &wlen, &snull, &sl, *p, *plen);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/** return true if the packet has EDNS OPT record */
|
|
static int
|
|
get_has_edns(uint8_t* pkt, size_t len)
|
|
{
|
|
/* use arguments as temporary variables */
|
|
return pkt_find_edns_opt(&pkt, &len);
|
|
}
|
|
|
|
/** return true if the DO flag is set */
|
|
static int
|
|
get_do_flag(uint8_t* pkt, size_t len)
|
|
{
|
|
uint16_t edns_bits;
|
|
uint8_t* walk = pkt;
|
|
size_t walk_len = len;
|
|
if(!pkt_find_edns_opt(&walk, &walk_len)) {
|
|
return 0;
|
|
}
|
|
if(walk_len < 6)
|
|
return 0; /* malformed */
|
|
edns_bits = sldns_read_uint16(walk+4);
|
|
return (int)(edns_bits&LDNS_EDNS_MASK_DO_BIT);
|
|
}
|
|
|
|
/** zero TTLs in packet */
|
|
static void
|
|
zerottls(uint8_t* pkt, size_t pktlen)
|
|
{
|
|
uint8_t* walk = pkt;
|
|
size_t walk_len = pktlen, sl=0;
|
|
char* snull = NULL;
|
|
uint16_t i;
|
|
uint16_t num = LDNS_ANCOUNT(pkt)+LDNS_NSCOUNT(pkt)+LDNS_ARCOUNT(pkt);
|
|
if(walk_len < LDNS_HEADER_SIZE)
|
|
return;
|
|
walk += LDNS_HEADER_SIZE;
|
|
walk_len -= LDNS_HEADER_SIZE;
|
|
for(i=0; i < LDNS_QDCOUNT(pkt); i++)
|
|
(void)sldns_wire2str_rrquestion_scan(&walk, &walk_len,
|
|
&snull, &sl, pkt, pktlen);
|
|
for(i=0; i < num; i++) {
|
|
/* wipe TTL */
|
|
uint8_t* dstart = walk;
|
|
size_t dlen = walk_len;
|
|
(void)sldns_wire2str_dname_scan(&dstart, &dlen, &snull, &sl,
|
|
pkt, pktlen);
|
|
if(dlen < 8)
|
|
return;
|
|
sldns_write_uint32(dstart+4, 0);
|
|
/* go to next RR */
|
|
(void)sldns_wire2str_rr_scan(&walk, &walk_len, &snull, &sl,
|
|
pkt, pktlen);
|
|
}
|
|
}
|
|
|
|
/** get one line (\n) from a string, move next to after the \n, zero \n */
|
|
static int
|
|
get_line(char** s, char** n)
|
|
{
|
|
/* at end of string? end */
|
|
if(*n == NULL || **n == 0)
|
|
return 0;
|
|
/* result starts at next string */
|
|
*s = *n;
|
|
/* find \n after that */
|
|
*n = strchr(*s, '\n');
|
|
if(*n && **n != 0) {
|
|
/* terminate line */
|
|
(*n)[0] = 0;
|
|
(*n)++;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/** match two RR sections without ordering */
|
|
static int
|
|
match_noloc_section(char** q, char** nq, char** p, char** np, uint16_t num)
|
|
{
|
|
/* for max number of RRs in packet */
|
|
const uint16_t numarray = 3000;
|
|
char* qlines[numarray], *plines[numarray];
|
|
uint16_t i, j, numq=0, nump=0;
|
|
if(num > numarray) fatal_exit("too many RRs");
|
|
/* gather lines */
|
|
for(i=0; i<num; i++) {
|
|
get_line(q, nq);
|
|
get_line(p, np);
|
|
qlines[numq++] = *q;
|
|
plines[nump++] = *p;
|
|
}
|
|
/* see if they are all present in the other */
|
|
for(i=0; i<num; i++) {
|
|
int found = 0;
|
|
for(j=0; j<num; j++) {
|
|
if(strcmp(qlines[i], plines[j]) == 0) {
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
if(!found) {
|
|
verbose(3, "comparenoloc: failed for %s", qlines[i]);
|
|
return 0;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/** match two strings for unordered equality of RRs and everything else */
|
|
static int
|
|
match_noloc(char* q, char* p, uint8_t* q_pkt, size_t q_pkt_len,
|
|
uint8_t* p_pkt, size_t p_pkt_len)
|
|
{
|
|
char* nq = q, *np = p;
|
|
/* if no header, compare bytes */
|
|
if(p_pkt_len < LDNS_HEADER_SIZE || q_pkt_len < LDNS_HEADER_SIZE) {
|
|
if(p_pkt_len != q_pkt_len) return 0;
|
|
return memcmp(p, q, p_pkt_len);
|
|
}
|
|
/* compare RR counts */
|
|
if(LDNS_QDCOUNT(p_pkt) != LDNS_QDCOUNT(q_pkt))
|
|
return 0;
|
|
if(LDNS_ANCOUNT(p_pkt) != LDNS_ANCOUNT(q_pkt))
|
|
return 0;
|
|
if(LDNS_NSCOUNT(p_pkt) != LDNS_NSCOUNT(q_pkt))
|
|
return 0;
|
|
if(LDNS_ARCOUNT(p_pkt) != LDNS_ARCOUNT(q_pkt))
|
|
return 0;
|
|
/* get a line from both; compare; at sections do section */
|
|
get_line(&q, &nq);
|
|
get_line(&p, &np);
|
|
if(strcmp(q, p) != 0) {
|
|
/* header line opcode, rcode, id */
|
|
return 0;
|
|
}
|
|
get_line(&q, &nq);
|
|
get_line(&p, &np);
|
|
if(strcmp(q, p) != 0) {
|
|
/* header flags, rr counts */
|
|
return 0;
|
|
}
|
|
/* ;; QUESTION SECTION */
|
|
get_line(&q, &nq);
|
|
get_line(&p, &np);
|
|
if(strcmp(q, p) != 0) return 0;
|
|
if(!match_noloc_section(&q, &nq, &p, &np, LDNS_QDCOUNT(p_pkt)))
|
|
return 0;
|
|
|
|
/* empty line and ;; ANSWER SECTION */
|
|
get_line(&q, &nq);
|
|
get_line(&p, &np);
|
|
if(strcmp(q, p) != 0) return 0;
|
|
get_line(&q, &nq);
|
|
get_line(&p, &np);
|
|
if(strcmp(q, p) != 0) return 0;
|
|
if(!match_noloc_section(&q, &nq, &p, &np, LDNS_ANCOUNT(p_pkt)))
|
|
return 0;
|
|
|
|
/* empty line and ;; AUTHORITY SECTION */
|
|
get_line(&q, &nq);
|
|
get_line(&p, &np);
|
|
if(strcmp(q, p) != 0) return 0;
|
|
get_line(&q, &nq);
|
|
get_line(&p, &np);
|
|
if(strcmp(q, p) != 0) return 0;
|
|
if(!match_noloc_section(&q, &nq, &p, &np, LDNS_NSCOUNT(p_pkt)))
|
|
return 0;
|
|
|
|
/* empty line and ;; ADDITIONAL SECTION */
|
|
get_line(&q, &nq);
|
|
get_line(&p, &np);
|
|
if(strcmp(q, p) != 0) return 0;
|
|
get_line(&q, &nq);
|
|
get_line(&p, &np);
|
|
if(strcmp(q, p) != 0) return 0;
|
|
if(!match_noloc_section(&q, &nq, &p, &np, LDNS_ARCOUNT(p_pkt)))
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/** lowercase domain name - does not follow compression pointers */
|
|
static void lowercase_dname(uint8_t** p, size_t* remain)
|
|
{
|
|
unsigned i, llen;
|
|
if(*remain == 0) return;
|
|
while(**p != 0) {
|
|
/* compressed? */
|
|
if((**p & 0xc0) == 0xc0) {
|
|
*p += 2;
|
|
*remain -= 2;
|
|
return;
|
|
}
|
|
llen = (unsigned int)**p;
|
|
*p += 1;
|
|
*remain -= 1;
|
|
if(*remain < llen)
|
|
llen = (unsigned int)*remain;
|
|
for(i=0; i<llen; i++) {
|
|
(*p)[i] = (uint8_t)tolower((int)(*p)[i]);
|
|
}
|
|
*p += llen;
|
|
*remain -= llen;
|
|
if(*remain == 0) return;
|
|
}
|
|
/* skip root label */
|
|
*p += 1;
|
|
*remain -= 1;
|
|
}
|
|
|
|
/** lowercase rdata of type */
|
|
static void lowercase_rdata(uint8_t** p, size_t* remain,
|
|
uint16_t rdatalen, uint16_t t)
|
|
{
|
|
const sldns_rr_descriptor *desc = sldns_rr_descript(t);
|
|
uint8_t dname_count = 0;
|
|
size_t i = 0;
|
|
size_t rdataremain = rdatalen;
|
|
if(!desc) {
|
|
/* unknown type */
|
|
*p += rdatalen;
|
|
*remain -= rdatalen;
|
|
return;
|
|
}
|
|
while(dname_count < desc->_dname_count) {
|
|
sldns_rdf_type f = sldns_rr_descriptor_field_type(desc, i++);
|
|
if(f == LDNS_RDF_TYPE_DNAME) {
|
|
lowercase_dname(p, &rdataremain);
|
|
dname_count++;
|
|
} else if(f == LDNS_RDF_TYPE_STR) {
|
|
uint8_t len;
|
|
if(rdataremain == 0) return;
|
|
len = **p;
|
|
*p += len+1;
|
|
rdataremain -= len+1;
|
|
} else {
|
|
int len = 0;
|
|
switch(f) {
|
|
case LDNS_RDF_TYPE_CLASS:
|
|
case LDNS_RDF_TYPE_ALG:
|
|
case LDNS_RDF_TYPE_INT8:
|
|
len = 1;
|
|
break;
|
|
case LDNS_RDF_TYPE_INT16:
|
|
case LDNS_RDF_TYPE_TYPE:
|
|
case LDNS_RDF_TYPE_CERT_ALG:
|
|
len = 2;
|
|
break;
|
|
case LDNS_RDF_TYPE_INT32:
|
|
case LDNS_RDF_TYPE_TIME:
|
|
case LDNS_RDF_TYPE_A:
|
|
case LDNS_RDF_TYPE_PERIOD:
|
|
len = 4;
|
|
break;
|
|
case LDNS_RDF_TYPE_TSIGTIME:
|
|
len = 6;
|
|
break;
|
|
case LDNS_RDF_TYPE_AAAA:
|
|
len = 16;
|
|
break;
|
|
default: error("bad rdf type in lowercase %d", (int)f);
|
|
}
|
|
*p += len;
|
|
rdataremain -= len;
|
|
}
|
|
}
|
|
/* skip remainder of rdata */
|
|
*p += rdataremain;
|
|
*remain -= rdatalen;
|
|
}
|
|
|
|
/** lowercase all names in the message */
|
|
static void lowercase_pkt(uint8_t* pkt, size_t pktlen)
|
|
{
|
|
uint16_t i;
|
|
uint8_t* p = pkt;
|
|
size_t remain = pktlen;
|
|
uint16_t t, rdatalen;
|
|
if(pktlen < LDNS_HEADER_SIZE)
|
|
return;
|
|
p += LDNS_HEADER_SIZE;
|
|
remain -= LDNS_HEADER_SIZE;
|
|
for(i=0; i<LDNS_QDCOUNT(pkt); i++) {
|
|
lowercase_dname(&p, &remain);
|
|
if(remain < 4) return;
|
|
p += 4;
|
|
remain -= 4;
|
|
}
|
|
for(i=0; i<LDNS_ANCOUNT(pkt)+LDNS_NSCOUNT(pkt)+LDNS_ARCOUNT(pkt); i++) {
|
|
lowercase_dname(&p, &remain);
|
|
if(remain < 10) return;
|
|
t = sldns_read_uint16(p);
|
|
rdatalen = sldns_read_uint16(p+8);
|
|
p += 10;
|
|
remain -= 10;
|
|
if(remain < rdatalen) return;
|
|
lowercase_rdata(&p, &remain, rdatalen, t);
|
|
}
|
|
}
|
|
|
|
/** match question section of packet */
|
|
static int
|
|
match_question(uint8_t* q, size_t qlen, uint8_t* p, size_t plen, int mttl)
|
|
{
|
|
char* qstr, *pstr, *s, *qcmpstr, *pcmpstr;
|
|
uint8_t* qb = q, *pb = p;
|
|
int r;
|
|
/* zero TTLs */
|
|
qb = memdup(q, qlen);
|
|
pb = memdup(p, plen);
|
|
if(!qb || !pb) error("out of memory");
|
|
if(!mttl) {
|
|
zerottls(qb, qlen);
|
|
zerottls(pb, plen);
|
|
}
|
|
lowercase_pkt(qb, qlen);
|
|
lowercase_pkt(pb, plen);
|
|
qstr = sldns_wire2str_pkt(qb, qlen);
|
|
pstr = sldns_wire2str_pkt(pb, plen);
|
|
if(!qstr || !pstr) error("cannot pkt2string");
|
|
|
|
/* remove before ;; QUESTION */
|
|
s = strstr(qstr, ";; QUESTION SECTION");
|
|
qcmpstr = s;
|
|
s = strstr(pstr, ";; QUESTION SECTION");
|
|
pcmpstr = s;
|
|
if(!qcmpstr && !pcmpstr) {
|
|
free(qstr);
|
|
free(pstr);
|
|
free(qb);
|
|
free(pb);
|
|
return 1;
|
|
}
|
|
if(!qcmpstr || !pcmpstr) {
|
|
free(qstr);
|
|
free(pstr);
|
|
free(qb);
|
|
free(pb);
|
|
return 0;
|
|
}
|
|
|
|
/* remove after answer section, (;; AUTH, ;; ADD, ;; MSG size ..) */
|
|
s = strstr(qcmpstr, ";; ANSWER SECTION");
|
|
if(!s) s = strstr(qcmpstr, ";; AUTHORITY SECTION");
|
|
if(!s) s = strstr(qcmpstr, ";; ADDITIONAL SECTION");
|
|
if(!s) s = strstr(qcmpstr, ";; MSG SIZE");
|
|
if(s) *s = 0;
|
|
s = strstr(pcmpstr, ";; ANSWER SECTION");
|
|
if(!s) s = strstr(pcmpstr, ";; AUTHORITY SECTION");
|
|
if(!s) s = strstr(pcmpstr, ";; ADDITIONAL SECTION");
|
|
if(!s) s = strstr(pcmpstr, ";; MSG SIZE");
|
|
if(s) *s = 0;
|
|
|
|
r = (strcmp(qcmpstr, pcmpstr) == 0);
|
|
|
|
if(!r) {
|
|
verbose(3, "mismatch question section '%s' and '%s'",
|
|
qcmpstr, pcmpstr);
|
|
}
|
|
|
|
free(qstr);
|
|
free(pstr);
|
|
free(qb);
|
|
free(pb);
|
|
return r;
|
|
}
|
|
|
|
/** match answer section of packet */
|
|
static int
|
|
match_answer(uint8_t* q, size_t qlen, uint8_t* p, size_t plen, int mttl)
|
|
{
|
|
char* qstr, *pstr, *s, *qcmpstr, *pcmpstr;
|
|
uint8_t* qb = q, *pb = p;
|
|
int r;
|
|
/* zero TTLs */
|
|
qb = memdup(q, qlen);
|
|
pb = memdup(p, plen);
|
|
if(!qb || !pb) error("out of memory");
|
|
if(!mttl) {
|
|
zerottls(qb, qlen);
|
|
zerottls(pb, plen);
|
|
}
|
|
lowercase_pkt(qb, qlen);
|
|
lowercase_pkt(pb, plen);
|
|
qstr = sldns_wire2str_pkt(qb, qlen);
|
|
pstr = sldns_wire2str_pkt(pb, plen);
|
|
if(!qstr || !pstr) error("cannot pkt2string");
|
|
|
|
/* remove before ;; ANSWER */
|
|
s = strstr(qstr, ";; ANSWER SECTION");
|
|
qcmpstr = s;
|
|
s = strstr(pstr, ";; ANSWER SECTION");
|
|
pcmpstr = s;
|
|
if(!qcmpstr && !pcmpstr) {
|
|
free(qstr);
|
|
free(pstr);
|
|
free(qb);
|
|
free(pb);
|
|
return 1;
|
|
}
|
|
if(!qcmpstr || !pcmpstr) {
|
|
free(qstr);
|
|
free(pstr);
|
|
free(qb);
|
|
free(pb);
|
|
return 0;
|
|
}
|
|
|
|
/* remove after answer section, (;; AUTH, ;; ADD, ;; MSG size ..) */
|
|
s = strstr(qcmpstr, ";; AUTHORITY SECTION");
|
|
if(!s) s = strstr(qcmpstr, ";; ADDITIONAL SECTION");
|
|
if(!s) s = strstr(qcmpstr, ";; MSG SIZE");
|
|
if(s) *s = 0;
|
|
s = strstr(pcmpstr, ";; AUTHORITY SECTION");
|
|
if(!s) s = strstr(pcmpstr, ";; ADDITIONAL SECTION");
|
|
if(!s) s = strstr(pcmpstr, ";; MSG SIZE");
|
|
if(s) *s = 0;
|
|
|
|
r = (strcmp(qcmpstr, pcmpstr) == 0);
|
|
|
|
if(!r) {
|
|
verbose(3, "mismatch answer section '%s' and '%s'",
|
|
qcmpstr, pcmpstr);
|
|
}
|
|
|
|
free(qstr);
|
|
free(pstr);
|
|
free(qb);
|
|
free(pb);
|
|
return r;
|
|
}
|
|
|
|
/** match all of the packet */
|
|
int
|
|
match_all(uint8_t* q, size_t qlen, uint8_t* p, size_t plen, int mttl,
|
|
int noloc)
|
|
{
|
|
char* qstr, *pstr;
|
|
uint8_t* qb = q, *pb = p;
|
|
int r;
|
|
/* zero TTLs */
|
|
qb = memdup(q, qlen);
|
|
pb = memdup(p, plen);
|
|
if(!qb || !pb) error("out of memory");
|
|
if(!mttl) {
|
|
zerottls(qb, qlen);
|
|
zerottls(pb, plen);
|
|
}
|
|
lowercase_pkt(qb, qlen);
|
|
lowercase_pkt(pb, plen);
|
|
qstr = sldns_wire2str_pkt(qb, qlen);
|
|
pstr = sldns_wire2str_pkt(pb, plen);
|
|
if(!qstr || !pstr) error("cannot pkt2string");
|
|
r = (strcmp(qstr, pstr) == 0);
|
|
if(!r) {
|
|
/* remove ;; MSG SIZE (at end of string) */
|
|
char* s = strstr(qstr, ";; MSG SIZE");
|
|
if(s) *s=0;
|
|
s = strstr(pstr, ";; MSG SIZE");
|
|
if(s) *s=0;
|
|
r = (strcmp(qstr, pstr) == 0);
|
|
if(!r && !noloc) {
|
|
/* we are going to fail see if it is because of EDNS */
|
|
char* a = strstr(qstr, "; EDNS");
|
|
char* b = strstr(pstr, "; EDNS");
|
|
if( (a&&!b) || (b&&!a) ) {
|
|
verbose(3, "mismatch in EDNS\n");
|
|
}
|
|
}
|
|
}
|
|
if(!r && noloc) {
|
|
/* check for reordered sections */
|
|
r = match_noloc(qstr, pstr, q, qlen, p, plen);
|
|
}
|
|
if(!r) {
|
|
verbose(3, "mismatch pkt '%s' and '%s'", qstr, pstr);
|
|
}
|
|
free(qstr);
|
|
free(pstr);
|
|
free(qb);
|
|
free(pb);
|
|
return r;
|
|
}
|
|
|
|
/** see if domain names are equal */
|
|
static int equal_dname(uint8_t* q, size_t qlen, uint8_t* p, size_t plen)
|
|
{
|
|
uint8_t* qn = get_qname(q, qlen);
|
|
uint8_t* pn = get_qname(p, plen);
|
|
char qs[512], ps[512];
|
|
size_t qslen = sizeof(qs), pslen = sizeof(ps);
|
|
char* qss = qs, *pss = ps;
|
|
if(!qn || !pn)
|
|
return 0;
|
|
(void)sldns_wire2str_dname_scan(&qn, &qlen, &qss, &qslen, q, qlen);
|
|
(void)sldns_wire2str_dname_scan(&pn, &plen, &pss, &pslen, p, plen);
|
|
return (strcmp(qs, ps) == 0);
|
|
}
|
|
|
|
/** see if domain names are subdomain q of p */
|
|
static int subdomain_dname(uint8_t* q, size_t qlen, uint8_t* p, size_t plen)
|
|
{
|
|
/* we use the tostring routines so as to test unbound's routines
|
|
* with something else */
|
|
uint8_t* qn = get_qname(q, qlen);
|
|
uint8_t* pn = get_qname(p, plen);
|
|
char qs[5120], ps[5120];
|
|
size_t qslen = sizeof(qs), pslen = sizeof(ps);
|
|
char* qss = qs, *pss = ps;
|
|
if(!qn || !pn)
|
|
return 0;
|
|
/* decompresses domain names */
|
|
(void)sldns_wire2str_dname_scan(&qn, &qlen, &qss, &qslen, q, qlen);
|
|
(void)sldns_wire2str_dname_scan(&pn, &plen, &pss, &pslen, p, plen);
|
|
/* same: false, (strict subdomain check)??? */
|
|
if(strcmp(qs, ps) == 0)
|
|
return 1;
|
|
/* qs must end in ps, at a dot, without \ in front */
|
|
qslen = strlen(qs);
|
|
pslen = strlen(ps);
|
|
if(qslen > pslen && strcmp(qs + (qslen-pslen), ps) == 0 &&
|
|
qslen + 2 >= pslen && /* space for label and dot */
|
|
qs[qslen-pslen-1] == '.') {
|
|
unsigned int slashcount = 0;
|
|
size_t i = qslen-pslen-2;
|
|
while(i>0 && qs[i]=='\\') {
|
|
i++;
|
|
slashcount++;
|
|
}
|
|
if(slashcount%1 == 1) return 0; /* . preceded by \ */
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/** Match OPT RDATA (not the EDNS payload size or flags) */
|
|
static int
|
|
match_ednsdata(uint8_t* q, size_t qlen, uint8_t* p, size_t plen)
|
|
{
|
|
uint8_t* walk_q = q;
|
|
size_t walk_qlen = qlen;
|
|
uint8_t* walk_p = p;
|
|
size_t walk_plen = plen;
|
|
|
|
if(!pkt_find_edns_opt(&walk_q, &walk_qlen))
|
|
walk_qlen = 0;
|
|
if(!pkt_find_edns_opt(&walk_p, &walk_plen))
|
|
walk_plen = 0;
|
|
|
|
/* class + ttl + rdlen = 8 */
|
|
if(walk_qlen <= 8 && walk_plen <= 8) {
|
|
verbose(3, "NO edns opt, move on");
|
|
return 1;
|
|
}
|
|
if(walk_qlen != walk_plen)
|
|
return 0;
|
|
|
|
return (memcmp(walk_p+8, walk_q+8, walk_qlen-8) == 0);
|
|
}
|
|
|
|
/* finds entry in list, or returns NULL */
|
|
struct entry*
|
|
find_match(struct entry* entries, uint8_t* query_pkt, size_t len,
|
|
enum transport_type transport)
|
|
{
|
|
struct entry* p = entries;
|
|
uint8_t* reply;
|
|
size_t rlen;
|
|
for(p=entries; p; p=p->next) {
|
|
verbose(3, "comparepkt: ");
|
|
reply = p->reply_list->reply_pkt;
|
|
rlen = p->reply_list->reply_len;
|
|
if(p->match_opcode && get_opcode(query_pkt, len) !=
|
|
get_opcode(reply, rlen)) {
|
|
verbose(3, "bad opcode\n");
|
|
continue;
|
|
}
|
|
if(p->match_qtype && get_qtype(query_pkt, len) !=
|
|
get_qtype(reply, rlen)) {
|
|
verbose(3, "bad qtype %d %d\n", get_qtype(query_pkt, len), get_qtype(reply, rlen));
|
|
continue;
|
|
}
|
|
if(p->match_qname) {
|
|
if(!equal_dname(query_pkt, len, reply, rlen)) {
|
|
verbose(3, "bad qname\n");
|
|
continue;
|
|
}
|
|
}
|
|
if(p->match_rcode) {
|
|
if(get_rcode(query_pkt, len) != get_rcode(reply, rlen)) {
|
|
char *r1 = sldns_wire2str_rcode(get_rcode(query_pkt, len));
|
|
char *r2 = sldns_wire2str_rcode(get_rcode(reply, rlen));
|
|
verbose(3, "bad rcode %s instead of %s\n",
|
|
r1, r2);
|
|
free(r1);
|
|
free(r2);
|
|
continue;
|
|
}
|
|
}
|
|
if(p->match_question) {
|
|
if(!match_question(query_pkt, len, reply, rlen,
|
|
(int)p->match_ttl)) {
|
|
verbose(3, "bad question section\n");
|
|
continue;
|
|
}
|
|
}
|
|
if(p->match_answer) {
|
|
if(!match_answer(query_pkt, len, reply, rlen,
|
|
(int)p->match_ttl)) {
|
|
verbose(3, "bad answer section\n");
|
|
continue;
|
|
}
|
|
}
|
|
if(p->match_subdomain) {
|
|
if(!subdomain_dname(query_pkt, len, reply, rlen)) {
|
|
verbose(3, "bad subdomain\n");
|
|
continue;
|
|
}
|
|
}
|
|
if(p->match_serial && get_serial(query_pkt, len) != p->ixfr_soa_serial) {
|
|
verbose(3, "bad serial\n");
|
|
continue;
|
|
}
|
|
if(p->match_do && !get_do_flag(query_pkt, len)) {
|
|
verbose(3, "no DO bit set\n");
|
|
continue;
|
|
}
|
|
if(p->match_noedns && get_has_edns(query_pkt, len)) {
|
|
verbose(3, "bad; EDNS OPT present\n");
|
|
continue;
|
|
}
|
|
if(p->match_ednsdata_raw &&
|
|
!match_ednsdata(query_pkt, len, reply, rlen)) {
|
|
verbose(3, "bad EDNS data match.\n");
|
|
continue;
|
|
}
|
|
if(p->match_transport != transport_any && p->match_transport != transport) {
|
|
verbose(3, "bad transport\n");
|
|
continue;
|
|
}
|
|
if(p->match_all && !match_all(query_pkt, len, reply, rlen,
|
|
(int)p->match_ttl, 0)) {
|
|
verbose(3, "bad allmatch\n");
|
|
continue;
|
|
}
|
|
verbose(3, "match!\n");
|
|
return p;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
adjust_packet(struct entry* match, uint8_t** answer_pkt, size_t *answer_len,
|
|
uint8_t* query_pkt, size_t query_len)
|
|
{
|
|
uint8_t* orig = *answer_pkt;
|
|
size_t origlen = *answer_len;
|
|
uint8_t* res;
|
|
size_t reslen;
|
|
|
|
/* perform the copy; if possible; must be uncompressed */
|
|
if(match->copy_query && origlen >= LDNS_HEADER_SIZE &&
|
|
query_len >= LDNS_HEADER_SIZE && LDNS_QDCOUNT(query_pkt)!=0
|
|
&& LDNS_QDCOUNT(orig)==0) {
|
|
/* no qname in output packet, insert it */
|
|
size_t dlen = get_qname_len(query_pkt, query_len);
|
|
reslen = origlen + dlen + 4;
|
|
res = (uint8_t*)malloc(reslen);
|
|
if(!res) {
|
|
verbose(1, "out of memory; send without adjust\n");
|
|
return;
|
|
}
|
|
/* copy the header, query, remainder */
|
|
memcpy(res, orig, LDNS_HEADER_SIZE);
|
|
memmove(res+LDNS_HEADER_SIZE, query_pkt+LDNS_HEADER_SIZE,
|
|
dlen+4);
|
|
memmove(res+LDNS_HEADER_SIZE+dlen+4, orig+LDNS_HEADER_SIZE,
|
|
reslen-(LDNS_HEADER_SIZE+dlen+4));
|
|
/* set QDCOUNT */
|
|
sldns_write_uint16(res+4, 1);
|
|
} else if(match->copy_query && origlen >= LDNS_HEADER_SIZE &&
|
|
query_len >= LDNS_HEADER_SIZE && LDNS_QDCOUNT(query_pkt)!=0
|
|
&& get_qname_len(orig, origlen) == 0) {
|
|
/* QDCOUNT(orig)!=0 but qlen == 0, therefore, an error */
|
|
verbose(1, "error: malformed qname; send without adjust\n");
|
|
res = memdup(orig, origlen);
|
|
reslen = origlen;
|
|
} else if(match->copy_query && origlen >= LDNS_HEADER_SIZE &&
|
|
query_len >= LDNS_HEADER_SIZE && LDNS_QDCOUNT(query_pkt)!=0
|
|
&& LDNS_QDCOUNT(orig)!=0) {
|
|
/* in this case olen != 0 and QDCOUNT(orig)!=0 */
|
|
/* copy query section */
|
|
size_t dlen = get_qname_len(query_pkt, query_len);
|
|
size_t olen = get_qname_len(orig, origlen);
|
|
reslen = origlen + dlen - olen;
|
|
res = (uint8_t*)malloc(reslen);
|
|
if(!res) {
|
|
verbose(1, "out of memory; send without adjust\n");
|
|
return;
|
|
}
|
|
/* copy the header, query, remainder */
|
|
memcpy(res, orig, LDNS_HEADER_SIZE);
|
|
memmove(res+LDNS_HEADER_SIZE, query_pkt+LDNS_HEADER_SIZE,
|
|
dlen+4);
|
|
memmove(res+LDNS_HEADER_SIZE+dlen+4,
|
|
orig+LDNS_HEADER_SIZE+olen+4,
|
|
reslen-(LDNS_HEADER_SIZE+dlen+4));
|
|
} else {
|
|
res = memdup(orig, origlen);
|
|
reslen = origlen;
|
|
}
|
|
if(!res) {
|
|
verbose(1, "out of memory; send without adjust\n");
|
|
return;
|
|
}
|
|
/* copy the ID */
|
|
if(match->copy_id && reslen >= 2)
|
|
res[1] = orig[1];
|
|
if(match->copy_id && reslen >= 1)
|
|
res[0] = orig[0];
|
|
|
|
if(match->copy_ednsdata_assume_clientsubnet) {
|
|
/** Assume there is only one EDNS option, which is ECS.
|
|
* Copy source mask from query to scope mask in reply. Assume
|
|
* rest of ECS data in response (eg address) matches the query.
|
|
*/
|
|
uint8_t* walk_q = orig;
|
|
size_t walk_qlen = origlen;
|
|
uint8_t* walk_p = res;
|
|
size_t walk_plen = reslen;
|
|
|
|
if(!pkt_find_edns_opt(&walk_q, &walk_qlen)) {
|
|
walk_qlen = 0;
|
|
}
|
|
if(!pkt_find_edns_opt(&walk_p, &walk_plen)) {
|
|
walk_plen = 0;
|
|
}
|
|
/* class + ttl + rdlen + optcode + optlen + ecs fam + ecs source
|
|
* + ecs scope = index 15 */
|
|
if(walk_qlen >= 15 && walk_plen >= 15) {
|
|
walk_p[15] = walk_q[14];
|
|
}
|
|
}
|
|
|
|
if(match->sleeptime > 0) {
|
|
verbose(3, "sleeping for %d seconds\n", match->sleeptime);
|
|
#ifdef HAVE_SLEEP
|
|
sleep(match->sleeptime);
|
|
#else
|
|
Sleep(match->sleeptime * 1000);
|
|
#endif
|
|
}
|
|
*answer_pkt = res;
|
|
*answer_len = reslen;
|
|
}
|
|
|
|
/*
|
|
* Parses data buffer to a query, finds the correct answer
|
|
* and calls the given function for every packet to send.
|
|
*/
|
|
void
|
|
handle_query(uint8_t* inbuf, ssize_t inlen, struct entry* entries, int* count,
|
|
enum transport_type transport, void (*sendfunc)(uint8_t*, size_t, void*),
|
|
void* userdata, FILE* verbose_out)
|
|
{
|
|
struct reply_packet *p;
|
|
uint8_t *outbuf = NULL;
|
|
size_t outlen = 0;
|
|
struct entry* entry = NULL;
|
|
|
|
verbose(1, "query %d: id %d: %s %d bytes: ", ++(*count),
|
|
(int)(inlen>=2?LDNS_ID_WIRE(inbuf):0),
|
|
(transport==transport_tcp)?"TCP":"UDP", (int)inlen);
|
|
if(verbose_out) {
|
|
char* out = sldns_wire2str_pkt(inbuf, (size_t)inlen);
|
|
printf("%s\n", out);
|
|
free(out);
|
|
}
|
|
|
|
/* fill up answer packet */
|
|
entry = find_match(entries, inbuf, (size_t)inlen, transport);
|
|
if(!entry || !entry->reply_list) {
|
|
verbose(1, "no answer packet for this query, no reply.\n");
|
|
return;
|
|
}
|
|
for(p = entry->reply_list; p; p = p->next)
|
|
{
|
|
verbose(3, "Answer pkt:\n");
|
|
if (p->reply_from_hex) {
|
|
/* try to adjust the hex packet, if it can be
|
|
* parsed, we can use adjust rules. if not,
|
|
* send packet literally */
|
|
/* still try to adjust ID if others fail */
|
|
outlen = sldns_buffer_limit(p->reply_from_hex);
|
|
outbuf = sldns_buffer_begin(p->reply_from_hex);
|
|
} else {
|
|
outbuf = p->reply_pkt;
|
|
outlen = p->reply_len;
|
|
}
|
|
if(!outbuf) {
|
|
verbose(1, "out of memory\n");
|
|
return;
|
|
}
|
|
/* copies outbuf in memory allocation */
|
|
adjust_packet(entry, &outbuf, &outlen, inbuf, (size_t)inlen);
|
|
verbose(1, "Answer packet size: %u bytes.\n", (unsigned int)outlen);
|
|
if(verbose_out) {
|
|
char* out = sldns_wire2str_pkt(outbuf, outlen);
|
|
printf("%s\n", out);
|
|
free(out);
|
|
}
|
|
if(p->packet_sleep) {
|
|
verbose(3, "sleeping for next packet %d secs\n",
|
|
p->packet_sleep);
|
|
#ifdef HAVE_SLEEP
|
|
sleep(p->packet_sleep);
|
|
#else
|
|
Sleep(p->packet_sleep * 1000);
|
|
#endif
|
|
verbose(3, "wakeup for next packet "
|
|
"(slept %d secs)\n", p->packet_sleep);
|
|
}
|
|
sendfunc(outbuf, outlen, userdata);
|
|
free(outbuf);
|
|
outbuf = NULL;
|
|
outlen = 0;
|
|
}
|
|
}
|
|
|
|
/** delete the list of reply packets */
|
|
void delete_replylist(struct reply_packet* replist)
|
|
{
|
|
struct reply_packet *p=replist, *np;
|
|
while(p) {
|
|
np = p->next;
|
|
free(p->reply_pkt);
|
|
sldns_buffer_free(p->reply_from_hex);
|
|
sldns_buffer_free(p->raw_ednsdata);
|
|
free(p);
|
|
p=np;
|
|
}
|
|
}
|
|
|
|
void delete_entry(struct entry* list)
|
|
{
|
|
struct entry *p=list, *np;
|
|
while(p) {
|
|
np = p->next;
|
|
delete_replylist(p->reply_list);
|
|
free(p);
|
|
p = np;
|
|
}
|
|
}
|