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