]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/zoneparser-tng.cc
Merge pull request #7909 from qvr/expungebyname-stats
[thirdparty/pdns.git] / pdns / zoneparser-tng.cc
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
39 static string g_INstr("IN");
40
41 ZoneParserTNG::ZoneParserTNG(const string& fname, const DNSName& zname, const string& reldir) : d_reldir(reldir),
42 d_zonename(zname), d_defaultttl(3600),
43 d_templatecounter(0), d_templatestop(0),
44 d_templatestep(0), d_havedollarttl(false){
45 stackFile(fname);
46 }
47
48 ZoneParserTNG::ZoneParserTNG(const vector<string> zonedata, const DNSName& zname):
49 d_zonename(zname), d_zonedata(zonedata), d_defaultttl(3600),
50 d_templatecounter(0), d_templatestop(0), d_templatestep(0),
51 d_havedollarttl(false), d_fromfile(false)
52 {
53 d_zonedataline = d_zonedata.begin();
54 }
55
56 void ZoneParserTNG::stackFile(const std::string& fname)
57 {
58 FILE *fp=fopen(fname.c_str(), "r");
59 if(!fp) {
60 std::error_code ec (errno,std::generic_category());
61 throw std::system_error(ec, "Unable to open file '"+fname+"': "+stringerror());
62 }
63
64 filestate fs(fp, fname);
65 d_filestates.push(fs);
66 d_fromfile = true;
67 }
68
69 ZoneParserTNG::~ZoneParserTNG()
70 {
71 while(!d_filestates.empty()) {
72 fclose(d_filestates.top().d_fp);
73 d_filestates.pop();
74 }
75 }
76
77 static string makeString(const string& line, const pair<string::size_type, string::size_type>& range)
78 {
79 return string(line.c_str() + range.first, range.second - range.first);
80 }
81
82 static bool isTimeSpec(const string& nextpart)
83 {
84 if(nextpart.empty())
85 return false;
86 for(string::const_iterator iter = nextpart.begin(); iter != nextpart.end(); ++iter) {
87 if(isdigit(*iter))
88 continue;
89 if(iter+1 != nextpart.end())
90 return false;
91 char c=tolower(*iter);
92 return (c=='s' || c=='m' || c=='h' || c=='d' || c=='w' || c=='y');
93 }
94 return true;
95 }
96
97
98 unsigned int ZoneParserTNG::makeTTLFromZone(const string& str)
99 {
100 if(str.empty())
101 return 0;
102
103 unsigned int val;
104 try {
105 val=pdns_stou(str);
106 }
107 catch (const std::out_of_range& oor) {
108 throw PDNSException("Unable to parse time specification '"+str+"' "+getLineOfFile());
109 }
110
111 char lc=dns_tolower(str[str.length()-1]);
112 if(!isdigit(lc))
113 switch(lc) {
114 case 's':
115 break;
116 case 'm':
117 val*=60; // minutes, not months!
118 break;
119 case 'h':
120 val*=3600;
121 break;
122 case 'd':
123 val*=3600*24;
124 break;
125 case 'w':
126 val*=3600*24*7;
127 break;
128 case 'y': // ? :-)
129 val*=3600*24*365;
130 break;
131
132 default:
133 throw PDNSException("Unable to parse time specification '"+str+"' "+getLineOfFile());
134 }
135 return val;
136 }
137
138 bool ZoneParserTNG::getTemplateLine()
139 {
140 if(d_templateparts.empty() || d_templatecounter > d_templatestop) // no template, or done with
141 return false;
142
143 string retline;
144 for(parts_t::const_iterator iter = d_templateparts.begin() ; iter != d_templateparts.end(); ++iter) {
145 if(iter != d_templateparts.begin())
146 retline+=" ";
147
148 string part=makeString(d_templateline, *iter);
149
150 /* a part can contain a 'naked' $, an escaped $ (\$), or ${offset,width,radix}, with width defaulting to 0,
151 and radix being 'd', 'o', 'x' or 'X', defaulting to 'd'.
152
153 The width is zero-padded, so if the counter is at 1, the offset is 15, with is 3, and the radix is 'x',
154 output will be '010', from the input of ${15,3,x}
155 */
156
157 string outpart;
158 outpart.reserve(part.size()+5);
159 bool inescape=false;
160
161 for(string::size_type pos = 0; pos < part.size() ; ++pos) {
162 char c=part[pos];
163 if(inescape) {
164 outpart.append(1, c);
165 inescape=false;
166 continue;
167 }
168
169 if(part[pos]=='\\') {
170 inescape=true;
171 continue;
172 }
173 if(c=='$') {
174 if(pos + 1 == part.size() || part[pos+1]!='{') { // a trailing $, or not followed by {
175 outpart.append(std::to_string(d_templatecounter));
176 continue;
177 }
178
179 // need to deal with { case
180
181 pos+=2;
182 string::size_type startPos=pos;
183 for(; pos < part.size() && part[pos]!='}' ; ++pos)
184 ;
185
186 if(pos == part.size()) // partial spec
187 break;
188
189 // we are on the '}'
190
191 string spec(part.c_str() + startPos, part.c_str() + pos);
192 int offset=0, width=0;
193 char radix='d';
194 sscanf(spec.c_str(), "%d,%d,%c", &offset, &width, &radix); // parse format specifier
195
196 char sformat[12];
197 snprintf(sformat, sizeof(sformat), "%%0%d%c", width, radix); // make into printf-style format
198
199 char tmp[80];
200 snprintf(tmp, sizeof(tmp), sformat, d_templatecounter + offset); // and do the actual printing
201 outpart+=tmp;
202 }
203 else
204 outpart.append(1, c);
205 }
206 retline+=outpart;
207 }
208 d_templatecounter+=d_templatestep;
209
210 d_line = retline;
211 return true;
212 }
213
214 void chopComment(string& line)
215 {
216 if(line.find(';')==string::npos)
217 return;
218 string::size_type pos, len = line.length();
219 bool inQuote=false;
220 for(pos = 0 ; pos < len; ++pos) {
221 if(line[pos]=='\\')
222 pos++;
223 else if(line[pos]=='"')
224 inQuote=!inQuote;
225 else if(line[pos]==';' && !inQuote)
226 break;
227 }
228 if(pos != len)
229 line.resize(pos);
230 }
231
232 bool findAndElide(string& line, char c)
233 {
234 string::size_type pos, len = line.length();
235 bool inQuote=false;
236 for(pos = 0 ; pos < len; ++pos) {
237 if(line[pos]=='\\')
238 pos++;
239 else if(line[pos]=='"')
240 inQuote=!inQuote;
241 else if(line[pos]==c && !inQuote)
242 break;
243 }
244 if(pos != len) {
245 line.erase(pos, 1);
246 return true;
247 }
248 return false;
249 }
250
251 DNSName ZoneParserTNG::getZoneName()
252 {
253 return d_zonename;
254 }
255
256 string ZoneParserTNG::getLineOfFile()
257 {
258 if (d_zonedata.size() > 0)
259 return "on line "+std::to_string(std::distance(d_zonedata.begin(), d_zonedataline))+" of given string";
260
261 if (d_filestates.empty())
262 return "";
263
264 return "on line "+std::to_string(d_filestates.top().d_lineno)+" of file '"+d_filestates.top().d_filename+"'";
265 }
266
267 pair<string,int> ZoneParserTNG::getLineNumAndFile()
268 {
269 if (d_filestates.empty())
270 return {"", 0};
271 else
272 return {d_filestates.top().d_filename, d_filestates.top().d_lineno};
273 }
274
275 bool ZoneParserTNG::get(DNSResourceRecord& rr, std::string* comment)
276 {
277 retry:;
278 if(!getTemplateLine() && !getLine())
279 return false;
280
281 boost::trim_right_if(d_line, is_any_of(" \t\r\n\x1a"));
282 if(comment)
283 comment->clear();
284 if(comment && d_line.find(';') != string::npos)
285 *comment = d_line.substr(d_line.find(';'));
286 parts_t parts;
287 vstringtok(parts, d_line);
288
289 if(parts.empty())
290 goto retry;
291
292 if(parts[0].first != parts[0].second && d_line[parts[0].first]==';') // line consisting of nothing but comments
293 goto retry;
294
295 if(d_line[0]=='$') {
296 string command=makeString(d_line, parts[0]);
297 if(pdns_iequals(command,"$TTL") && parts.size() > 1) {
298 d_defaultttl=makeTTLFromZone(trim_right_copy_if(makeString(d_line, parts[1]), is_any_of(";")));
299 d_havedollarttl=true;
300 }
301 else if(pdns_iequals(command,"$INCLUDE") && parts.size() > 1 && d_fromfile) {
302 string fname=unquotify(makeString(d_line, parts[1]));
303 if(!fname.empty() && fname[0]!='/' && !d_reldir.empty())
304 fname=d_reldir+"/"+fname;
305 stackFile(fname);
306 }
307 else if(pdns_iequals(command, "$ORIGIN") && parts.size() > 1) {
308 d_zonename = DNSName(makeString(d_line, parts[1]));
309 }
310 else if(pdns_iequals(command, "$GENERATE") && parts.size() > 2) {
311 // $GENERATE 1-127 $ CNAME $.0
312 string range=makeString(d_line, parts[1]);
313 d_templatestep=1;
314 d_templatestop=0;
315 sscanf(range.c_str(),"%u-%u/%u", &d_templatecounter, &d_templatestop, &d_templatestep);
316 d_templateline=d_line;
317 parts.pop_front();
318 parts.pop_front();
319
320 d_templateparts=parts;
321 goto retry;
322 }
323 else
324 throw exception("Can't parse zone line '"+d_line+"' "+getLineOfFile());
325 goto retry;
326 }
327
328 bool prevqname=false;
329 string qname = makeString(d_line, parts[0]); // Don't use DNSName here!
330 if(dns_isspace(d_line[0])) {
331 rr.qname=d_prevqname;
332 prevqname=true;
333 }else {
334 rr.qname=DNSName(qname);
335 parts.pop_front();
336 if(qname.empty() || qname[0]==';')
337 goto retry;
338 }
339 if(qname=="@")
340 rr.qname=d_zonename;
341 else if(!prevqname && !isCanonical(qname))
342 rr.qname += d_zonename;
343 d_prevqname=rr.qname;
344
345 if(parts.empty())
346 throw exception("Line with too little parts "+getLineOfFile());
347
348 string nextpart;
349
350 rr.ttl=d_defaultttl;
351 bool haveTTL=0, haveQTYPE=0;
352 pair<string::size_type, string::size_type> range;
353
354 while(!parts.empty()) {
355 range=parts.front();
356 parts.pop_front();
357 nextpart=makeString(d_line, range);
358 if(nextpart.empty())
359 break;
360
361 if(nextpart.find(';')!=string::npos) {
362 break;
363 }
364
365 // cout<<"Next part: '"<<nextpart<<"'"<<endl;
366
367 if(pdns_iequals(nextpart, g_INstr)) {
368 // cout<<"Ignoring 'IN'\n";
369 continue;
370 }
371 if(!haveTTL && !haveQTYPE && isTimeSpec(nextpart)) {
372 rr.ttl=makeTTLFromZone(nextpart);
373 if(!d_havedollarttl)
374 d_defaultttl = rr.ttl;
375 haveTTL=true;
376 // cout<<"ttl is probably: "<<rr.ttl<<endl;
377 continue;
378 }
379 if(haveQTYPE)
380 break;
381
382 try {
383 rr.qtype=DNSRecordContent::TypeToNumber(nextpart);
384 // cout<<"Got qtype ("<<rr.qtype.getCode()<<")\n";
385 haveQTYPE=1;
386 continue;
387 }
388 catch(...) {
389 throw runtime_error("Parsing zone content "+getLineOfFile()+
390 ": '"+nextpart+
391 "' doesn't look like a qtype, stopping loop");
392 }
393 }
394 if(!haveQTYPE)
395 throw exception("Malformed line "+getLineOfFile()+": '"+d_line+"'");
396
397 // rr.content=d_line.substr(range.first);
398 rr.content.assign(d_line, range.first, string::npos);
399 chopComment(rr.content);
400 trim_if(rr.content, is_any_of(" \r\n\t\x1a"));
401
402 if(rr.content.size()==1 && rr.content[0]=='@')
403 rr.content=d_zonename.toString();
404
405 if(findAndElide(rr.content, '(')) { // have found a ( and elided it
406 if(!findAndElide(rr.content, ')')) {
407 while(getLine()) {
408 trim_right(d_line);
409 chopComment(d_line);
410 trim(d_line);
411
412 bool ended = findAndElide(d_line, ')');
413 rr.content+=" "+d_line;
414 if(ended)
415 break;
416 }
417 }
418 }
419 trim_if(rr.content, is_any_of(" \r\n\t\x1a"));
420
421 vector<string> recparts;
422 switch(rr.qtype.getCode()) {
423 case QType::MX:
424 stringtok(recparts, rr.content);
425 if(recparts.size()==2) {
426 if (recparts[1]!=".") {
427 try {
428 recparts[1] = toCanonic(d_zonename, recparts[1]).toStringRootDot();
429 } catch (std::exception &e) {
430 throw PDNSException("Error in record '" + rr.qname.toLogString() + " " + rr.qtype.getName() + "': " + e.what());
431 }
432 }
433 rr.content=recparts[0]+" "+recparts[1];
434 }
435 break;
436
437 case QType::RP:
438 stringtok(recparts, rr.content);
439 if(recparts.size()==2) {
440 recparts[0] = toCanonic(d_zonename, recparts[0]).toStringRootDot();
441 recparts[1] = toCanonic(d_zonename, recparts[1]).toStringRootDot();
442 rr.content=recparts[0]+" "+recparts[1];
443 }
444 break;
445
446 case QType::SRV:
447 stringtok(recparts, rr.content);
448 if(recparts.size()==4) {
449 if(recparts[3]!=".") {
450 try {
451 recparts[3] = toCanonic(d_zonename, recparts[3]).toStringRootDot();
452 } catch (std::exception &e) {
453 throw PDNSException("Error in record '" + rr.qname.toLogString() + " " + rr.qtype.getName() + "': " + e.what());
454 }
455 }
456 rr.content=recparts[0]+" "+recparts[1]+" "+recparts[2]+" "+recparts[3];
457 }
458 break;
459
460
461 case QType::NS:
462 case QType::CNAME:
463 case QType::DNAME:
464 case QType::PTR:
465 try {
466 rr.content = toCanonic(d_zonename, rr.content).toStringRootDot();
467 } catch (std::exception &e) {
468 throw PDNSException("Error in record '" + rr.qname.toLogString() + " " + rr.qtype.getName() + "': " + e.what());
469 }
470 break;
471 case QType::AFSDB:
472 stringtok(recparts, rr.content);
473 if(recparts.size() == 2) {
474 try {
475 recparts[1]=toCanonic(d_zonename, recparts[1]).toStringRootDot();
476 } catch (std::exception &e) {
477 throw PDNSException("Error in record '" + rr.qname.toLogString() + " " + rr.qtype.getName() + "': " + e.what());
478 }
479 } else {
480 throw PDNSException("AFSDB record for "+rr.qname.toLogString()+" invalid");
481 }
482 rr.content.clear();
483 for(string::size_type n = 0; n < recparts.size(); ++n) {
484 if(n)
485 rr.content.append(1,' ');
486
487 rr.content+=recparts[n];
488 }
489 break;
490 case QType::SOA:
491 stringtok(recparts, rr.content);
492 if(recparts.size() > 7)
493 throw PDNSException("SOA record contents for "+rr.qname.toLogString()+" contains too many parts");
494 if(recparts.size() > 1) {
495 try {
496 recparts[0]=toCanonic(d_zonename, recparts[0]).toStringRootDot();
497 recparts[1]=toCanonic(d_zonename, recparts[1]).toStringRootDot();
498 } catch (std::exception &e) {
499 throw PDNSException("Error in record '" + rr.qname.toLogString() + " " + rr.qtype.getName() + "': " + e.what());
500 }
501 }
502 rr.content.clear();
503 for(string::size_type n = 0; n < recparts.size(); ++n) {
504 if(n)
505 rr.content.append(1,' ');
506
507 if(n > 1)
508 rr.content+=std::to_string(makeTTLFromZone(recparts[n]));
509 else
510 rr.content+=recparts[n];
511 }
512 break;
513 default:;
514 }
515 return true;
516 }
517
518
519 bool ZoneParserTNG::getLine()
520 {
521 if (d_zonedata.size() > 0) {
522 if (d_zonedataline != d_zonedata.end()) {
523 d_line = *d_zonedataline;
524 ++d_zonedataline;
525 return true;
526 }
527 return false;
528 }
529 while(!d_filestates.empty()) {
530 if(stringfgets(d_filestates.top().d_fp, d_line)) {
531 d_filestates.top().d_lineno++;
532 return true;
533 }
534 fclose(d_filestates.top().d_fp);
535 d_filestates.pop();
536 }
537 return false;
538 }