1 /*
2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
8 *
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 */
22 #ifdef HAVE_CONFIG_H
23 #include "config.h"
24 #endif
25 #include "ascii.hh"
26 #include "dnsparser.hh"
27 #include "sstuff.hh"
28 #include "misc.hh"
29 #include "dnswriter.hh"
30 #include "dnsrecords.hh"
31 #include "misc.hh"
32 #include <fstream>
33 #include "dns.hh"
34 #include "zoneparser-tng.hh"
35 #include <deque>
36 #include <boost/algorithm/string.hpp>
37 #include <system_error>
38 #include <cinttypes>
39
40 static string g_INstr("IN");
41
ZoneParserTNG(const string & fname,DNSName zname,string reldir,bool upgradeContent)42 ZoneParserTNG::ZoneParserTNG(const string& fname, DNSName zname, string reldir, bool upgradeContent):
43 d_reldir(std::move(reldir)), d_zonename(std::move(zname)), d_defaultttl(3600),
44 d_templatecounter(0), d_templatestop(0), d_templatestep(0),
45 d_havedollarttl(false), d_fromfile(true), d_upgradeContent(upgradeContent)
46 {
47 stackFile(fname);
48 }
49
ZoneParserTNG(const vector<string> & zonedata,DNSName zname,bool upgradeContent)50 ZoneParserTNG::ZoneParserTNG(const vector<string>& zonedata, DNSName zname, bool upgradeContent):
51 d_zonename(std::move(zname)), d_zonedata(zonedata), d_defaultttl(3600),
52 d_templatecounter(0), d_templatestop(0), d_templatestep(0),
53 d_havedollarttl(false), d_fromfile(false), d_upgradeContent(upgradeContent)
54 {
55 d_zonedataline = d_zonedata.begin();
56 }
57
stackFile(const std::string & fname)58 void ZoneParserTNG::stackFile(const std::string& fname)
59 {
60 FILE *fp=fopen(fname.c_str(), "r");
61 if(!fp) {
62 std::error_code ec (errno,std::generic_category());
63 throw std::system_error(ec, "Unable to open file '"+fname+"': "+stringerror());
64 }
65
66 filestate fs(fp, fname);
67 d_filestates.push(fs);
68 d_fromfile = true;
69 }
70
~ZoneParserTNG()71 ZoneParserTNG::~ZoneParserTNG()
72 {
73 while(!d_filestates.empty()) {
74 fclose(d_filestates.top().d_fp);
75 d_filestates.pop();
76 }
77 }
78
makeString(const string & line,const pair<string::size_type,string::size_type> & range)79 static string makeString(const string& line, const pair<string::size_type, string::size_type>& range)
80 {
81 return string(line.c_str() + range.first, range.second - range.first);
82 }
83
isTimeSpec(const string & nextpart)84 static bool isTimeSpec(const string& nextpart)
85 {
86 if(nextpart.empty())
87 return false;
88 for(string::const_iterator iter = nextpart.begin(); iter != nextpart.end(); ++iter) {
89 if(isdigit(*iter))
90 continue;
91 if(iter+1 != nextpart.end())
92 return false;
93 char c=tolower(*iter);
94 return (c=='s' || c=='m' || c=='h' || c=='d' || c=='w' || c=='y');
95 }
96 return true;
97 }
98
99
makeTTLFromZone(const string & str)100 unsigned int ZoneParserTNG::makeTTLFromZone(const string& str)
101 {
102 if(str.empty())
103 return 0;
104
105 unsigned int val;
106 try {
107 val=pdns_stou(str);
108 }
109 catch (const std::out_of_range& oor) {
110 throw PDNSException("Unable to parse time specification '"+str+"' "+getLineOfFile());
111 }
112
113 char lc=dns_tolower(str[str.length()-1]);
114 if(!isdigit(lc))
115 switch(lc) {
116 case 's':
117 break;
118 case 'm':
119 val*=60; // minutes, not months!
120 break;
121 case 'h':
122 val*=3600;
123 break;
124 case 'd':
125 val*=3600*24;
126 break;
127 case 'w':
128 val*=3600*24*7;
129 break;
130 case 'y': // ? :-)
131 val*=3600*24*365;
132 break;
133
134 default:
135 throw PDNSException("Unable to parse time specification '"+str+"' "+getLineOfFile());
136 }
137 return val;
138 }
139
getTemplateLine()140 bool ZoneParserTNG::getTemplateLine()
141 {
142 if(d_templateparts.empty() || d_templatecounter > d_templatestop) // no template, or done with
143 return false;
144
145 string retline;
146 for(parts_t::const_iterator iter = d_templateparts.begin() ; iter != d_templateparts.end(); ++iter) {
147 if(iter != d_templateparts.begin())
148 retline+=" ";
149
150 string part=makeString(d_templateline, *iter);
151
152 /* a part can contain a 'naked' $, an escaped $ (\$), or ${offset,width,radix}, with width defaulting to 0,
153 and radix being 'd', 'o', 'x' or 'X', defaulting to 'd' (so ${offset} is valid).
154
155 The width is zero-padded, so if the counter is at 1, the offset is 15, width is 3, and the radix is 'x',
156 output will be '010', from the input of ${15,3,x}
157 */
158
159 string outpart;
160 outpart.reserve(part.size()+5);
161 bool inescape=false;
162
163 for(string::size_type pos = 0; pos < part.size() ; ++pos) {
164 char c=part[pos];
165 if(inescape) {
166 outpart.append(1, c);
167 inescape=false;
168 continue;
169 }
170
171 if(part[pos]=='\\') {
172 inescape=true;
173 continue;
174 }
175 if(c=='$') {
176 if(pos + 1 == part.size() || part[pos+1]!='{') { // a trailing $, or not followed by {
177 outpart.append(std::to_string(d_templatecounter));
178 continue;
179 }
180
181 // need to deal with { case
182
183 pos+=2;
184 string::size_type startPos=pos;
185 for(; pos < part.size() && part[pos]!='}' ; ++pos)
186 ;
187
188 if(pos == part.size()) // partial spec
189 break;
190
191 // we are on the '}'
192
193 string spec(part.c_str() + startPos, part.c_str() + pos);
194 int offset=0, width=0;
195 char radix='d';
196 // parse format specifier
197 int extracted = sscanf(spec.c_str(), "%d,%d,%c", &offset, &width, &radix);
198 if (extracted < 1) {
199 throw PDNSException("Unable to parse offset, width and radix for $GENERATE's lhs from '"+spec+"' "+getLineOfFile());
200 }
201
202 char tmp[80];
203 switch (radix) {
204 case 'o':
205 snprintf(tmp, sizeof(tmp), "%0*o", width, d_templatecounter + offset);
206 break;
207 case 'x':
208 snprintf(tmp, sizeof(tmp), "%0*x", width, d_templatecounter + offset);
209 break;
210 case 'X':
211 snprintf(tmp, sizeof(tmp), "%0*X", width, d_templatecounter + offset);
212 break;
213 case 'd':
214 default:
215 snprintf(tmp, sizeof(tmp), "%0*d", width, d_templatecounter + offset);
216 break;
217 }
218 outpart+=tmp;
219 }
220 else
221 outpart.append(1, c);
222 }
223 retline+=outpart;
224 }
225 d_templatecounter+=d_templatestep;
226
227 d_line = retline;
228 return true;
229 }
230
chopComment(string & line)231 static void chopComment(string& line)
232 {
233 if(line.find(';')==string::npos)
234 return;
235 string::size_type pos, len = line.length();
236 bool inQuote=false;
237 for(pos = 0 ; pos < len; ++pos) {
238 if(line[pos]=='\\')
239 pos++;
240 else if(line[pos]=='"')
241 inQuote=!inQuote;
242 else if(line[pos]==';' && !inQuote)
243 break;
244 }
245 if(pos != len)
246 line.resize(pos);
247 }
248
findAndElide(string & line,char c)249 static bool findAndElide(string& line, char c)
250 {
251 string::size_type pos, len = line.length();
252 bool inQuote=false;
253 for(pos = 0 ; pos < len; ++pos) {
254 if(line[pos]=='\\')
255 pos++;
256 else if(line[pos]=='"')
257 inQuote=!inQuote;
258 else if(line[pos]==c && !inQuote)
259 break;
260 }
261 if(pos != len) {
262 line.erase(pos, 1);
263 return true;
264 }
265 return false;
266 }
267
getZoneName()268 DNSName ZoneParserTNG::getZoneName()
269 {
270 return d_zonename;
271 }
272
getLineOfFile()273 string ZoneParserTNG::getLineOfFile()
274 {
275 if (d_zonedata.size() > 0)
276 return "on line "+std::to_string(std::distance(d_zonedata.begin(), d_zonedataline))+" of given string";
277
278 if (d_filestates.empty())
279 return "";
280
281 return "on line "+std::to_string(d_filestates.top().d_lineno)+" of file '"+d_filestates.top().d_filename+"'";
282 }
283
getLineNumAndFile()284 pair<string,int> ZoneParserTNG::getLineNumAndFile()
285 {
286 if (d_filestates.empty())
287 return {"", 0};
288 else
289 return {d_filestates.top().d_filename, d_filestates.top().d_lineno};
290 }
291
get(DNSResourceRecord & rr,std::string * comment)292 bool ZoneParserTNG::get(DNSResourceRecord& rr, std::string* comment)
293 {
294 retry:;
295 if(!getTemplateLine() && !getLine())
296 return false;
297
298 boost::trim_right_if(d_line, boost::is_any_of(" \t\r\n\x1a"));
299 if(comment)
300 comment->clear();
301 if(comment && d_line.find(';') != string::npos)
302 *comment = d_line.substr(d_line.find(';'));
303
304 d_parts.clear();
305 vstringtok(d_parts, d_line);
306
307 if(d_parts.empty())
308 goto retry;
309
310 if(d_parts[0].first != d_parts[0].second && d_line[d_parts[0].first]==';') // line consisting of nothing but comments
311 goto retry;
312
313 if(d_line[0]=='$') {
314 string command=makeString(d_line, d_parts[0]);
315 if(pdns_iequals(command,"$TTL") && d_parts.size() > 1) {
316 d_defaultttl=makeTTLFromZone(trim_right_copy_if(makeString(d_line, d_parts[1]), boost::is_any_of(";")));
317 d_havedollarttl=true;
318 }
319 else if(pdns_iequals(command,"$INCLUDE") && d_parts.size() > 1 && d_fromfile) {
320 string fname=unquotify(makeString(d_line, d_parts[1]));
321 if(!fname.empty() && fname[0]!='/' && !d_reldir.empty())
322 fname=d_reldir+"/"+fname;
323 stackFile(fname);
324 }
325 else if(pdns_iequals(command, "$ORIGIN") && d_parts.size() > 1) {
326 d_zonename = DNSName(makeString(d_line, d_parts[1]));
327 }
328 else if(pdns_iequals(command, "$GENERATE") && d_parts.size() > 2) {
329 if (!d_generateEnabled) {
330 throw exception("$GENERATE is not allowed in this zone");
331 }
332 // $GENERATE 1-127 $ CNAME $.0
333 // The range part can be one of two forms: start-stop or start-stop/step. If the first
334 // form is used, then step is set to 1. start, stop and step must be positive
335 // integers between 0 and (2^31)-1. start must not be larger than stop.
336 string range=makeString(d_line, d_parts[1]);
337 d_templatestep=1;
338 d_templatestop=0;
339 int extracted = sscanf(range.c_str(),"%" SCNu32 "-%" SCNu32 "/%" SCNu32, &d_templatecounter, &d_templatestop, &d_templatestep);
340 if (extracted == 2) {
341 d_templatestep=1;
342 }
343 else if (extracted != 3) {
344 throw exception("Invalid range from $GENERATE parameters '" + range + "'");
345 }
346 if (d_templatestep < 1 ||
347 d_templatestop < d_templatecounter) {
348 throw exception("Invalid $GENERATE parameters");
349 }
350 if (d_maxGenerateSteps != 0) {
351 size_t numberOfSteps = (d_templatestop - d_templatecounter) / d_templatestep;
352 if (numberOfSteps > d_maxGenerateSteps) {
353 throw exception("The number of $GENERATE steps (" + std::to_string(numberOfSteps) + ") is too high, the maximum is set to " + std::to_string(d_maxGenerateSteps));
354 }
355 }
356 d_templateline=d_line;
357 d_parts.pop_front();
358 d_parts.pop_front();
359
360 d_templateparts=d_parts;
361 goto retry;
362 }
363 else
364 throw exception("Can't parse zone line '"+d_line+"' "+getLineOfFile());
365 goto retry;
366 }
367
368 bool prevqname=false;
369 string qname = makeString(d_line, d_parts[0]); // Don't use DNSName here!
370 if(dns_isspace(d_line[0])) {
371 rr.qname=d_prevqname;
372 prevqname=true;
373 }else {
374 rr.qname=DNSName(qname);
375 d_parts.pop_front();
376 if(qname.empty() || qname[0]==';')
377 goto retry;
378 }
379 if(qname=="@")
380 rr.qname=d_zonename;
381 else if(!prevqname && !isCanonical(qname))
382 rr.qname += d_zonename;
383 d_prevqname=rr.qname;
384
385 if(d_parts.empty())
386 throw exception("Line with too little parts "+getLineOfFile());
387
388 string nextpart;
389
390 rr.ttl=d_defaultttl;
391 bool haveTTL{false}, haveQTYPE{false};
392 string qtypeString;
393 pair<string::size_type, string::size_type> range;
394
395 while(!d_parts.empty()) {
396 range=d_parts.front();
397 d_parts.pop_front();
398 nextpart=makeString(d_line, range);
399 if(nextpart.empty())
400 break;
401
402 if(nextpart.find(';')!=string::npos) {
403 break;
404 }
405
406 // cout<<"Next part: '"<<nextpart<<"'"<<endl;
407
408 if(pdns_iequals(nextpart, g_INstr)) {
409 // cout<<"Ignoring 'IN'\n";
410 continue;
411 }
412 if(!haveTTL && !haveQTYPE && isTimeSpec(nextpart)) {
413 rr.ttl=makeTTLFromZone(nextpart);
414 if(!d_havedollarttl)
415 d_defaultttl = rr.ttl;
416 haveTTL=true;
417 // cout<<"ttl is probably: "<<rr.ttl<<endl;
418 continue;
419 }
420 if(haveQTYPE)
421 break;
422
423 try {
424 rr.qtype = DNSRecordContent::TypeToNumber(nextpart);
425 // cout<<"Got qtype ("<<rr.qtype.getCode()<<")\n";
426 qtypeString = nextpart;
427 haveQTYPE = true;
428 continue;
429 }
430 catch(...) {
431 throw runtime_error("Parsing zone content "+getLineOfFile()+
432 ": '"+nextpart+
433 "' doesn't look like a qtype, stopping loop");
434 }
435 }
436 if(!haveQTYPE)
437 throw exception("Malformed line "+getLineOfFile()+": '"+d_line+"'");
438
439 // rr.content=d_line.substr(range.first);
440 rr.content.assign(d_line, range.first, string::npos);
441 chopComment(rr.content);
442 trim_if(rr.content, boost::is_any_of(" \r\n\t\x1a"));
443
444 if(rr.content.size()==1 && rr.content[0]=='@')
445 rr.content=d_zonename.toString();
446
447 if(findAndElide(rr.content, '(')) { // have found a ( and elided it
448 if(!findAndElide(rr.content, ')')) {
449 while(getLine()) {
450 boost::trim_right(d_line);
451 chopComment(d_line);
452 boost::trim(d_line);
453
454 bool ended = findAndElide(d_line, ')');
455 rr.content+=" "+d_line;
456 if(ended)
457 break;
458 }
459 }
460 }
461 boost::trim_if(rr.content, boost::is_any_of(" \r\n\t\x1a"));
462
463 if (d_upgradeContent && DNSRecordContent::isUnknownType(qtypeString)) {
464 rr.content = DNSRecordContent::upgradeContent(rr.qname, rr.qtype, rr.content);
465 }
466
467 vector<string> recparts;
468 switch(rr.qtype.getCode()) {
469 case QType::MX:
470 stringtok(recparts, rr.content);
471 if(recparts.size()==2) {
472 if (recparts[1]!=".") {
473 try {
474 recparts[1] = toCanonic(d_zonename, recparts[1]).toStringRootDot();
475 } catch (std::exception &e) {
476 throw PDNSException("Error in record '" + rr.qname.toLogString() + " " + rr.qtype.toString() + "': " + e.what());
477 }
478 }
479 rr.content=recparts[0]+" "+recparts[1];
480 }
481 break;
482
483 case QType::RP:
484 stringtok(recparts, rr.content);
485 if(recparts.size()==2) {
486 recparts[0] = toCanonic(d_zonename, recparts[0]).toStringRootDot();
487 recparts[1] = toCanonic(d_zonename, recparts[1]).toStringRootDot();
488 rr.content=recparts[0]+" "+recparts[1];
489 }
490 break;
491
492 case QType::SRV:
493 stringtok(recparts, rr.content);
494 if(recparts.size()==4) {
495 if(recparts[3]!=".") {
496 try {
497 recparts[3] = toCanonic(d_zonename, recparts[3]).toStringRootDot();
498 } catch (std::exception &e) {
499 throw PDNSException("Error in record '" + rr.qname.toLogString() + " " + rr.qtype.toString() + "': " + e.what());
500 }
501 }
502 rr.content=recparts[0]+" "+recparts[1]+" "+recparts[2]+" "+recparts[3];
503 }
504 break;
505
506
507 case QType::NS:
508 case QType::CNAME:
509 case QType::DNAME:
510 case QType::PTR:
511 try {
512 rr.content = toCanonic(d_zonename, rr.content).toStringRootDot();
513 } catch (std::exception &e) {
514 throw PDNSException("Error in record '" + rr.qname.toLogString() + " " + rr.qtype.toString() + "': " + e.what());
515 }
516 break;
517 case QType::AFSDB:
518 stringtok(recparts, rr.content);
519 if(recparts.size() == 2) {
520 try {
521 recparts[1]=toCanonic(d_zonename, recparts[1]).toStringRootDot();
522 } catch (std::exception &e) {
523 throw PDNSException("Error in record '" + rr.qname.toLogString() + " " + rr.qtype.toString() + "': " + e.what());
524 }
525 } else {
526 throw PDNSException("AFSDB record for "+rr.qname.toLogString()+" invalid");
527 }
528 rr.content.clear();
529 for(string::size_type n = 0; n < recparts.size(); ++n) {
530 if(n)
531 rr.content.append(1,' ');
532
533 rr.content+=recparts[n];
534 }
535 break;
536 case QType::SOA:
537 stringtok(recparts, rr.content);
538 if(recparts.size() > 7)
539 throw PDNSException("SOA record contents for "+rr.qname.toLogString()+" contains too many parts");
540 if(recparts.size() > 1) {
541 try {
542 recparts[0]=toCanonic(d_zonename, recparts[0]).toStringRootDot();
543 recparts[1]=toCanonic(d_zonename, recparts[1]).toStringRootDot();
544 } catch (std::exception &e) {
545 throw PDNSException("Error in record '" + rr.qname.toLogString() + " " + rr.qtype.toString() + "': " + e.what());
546 }
547 }
548 rr.content.clear();
549 for(string::size_type n = 0; n < recparts.size(); ++n) {
550 if(n)
551 rr.content.append(1,' ');
552
553 if(n > 1)
554 rr.content+=std::to_string(makeTTLFromZone(recparts[n]));
555 else
556 rr.content+=recparts[n];
557 }
558 break;
559 default:;
560 }
561 return true;
562 }
563
564
getLine()565 bool ZoneParserTNG::getLine()
566 {
567 if (d_zonedata.size() > 0) {
568 if (d_zonedataline != d_zonedata.end()) {
569 d_line = *d_zonedataline;
570 ++d_zonedataline;
571 return true;
572 }
573 return false;
574 }
575 while(!d_filestates.empty()) {
576 if(stringfgets(d_filestates.top().d_fp, d_line)) {
577 d_filestates.top().d_lineno++;
578 return true;
579 }
580 fclose(d_filestates.top().d_fp);
581 d_filestates.pop();
582 }
583 return false;
584 }
585