Add new ZoneParser API (#794)
* Improve ParseZone tests
* Add new ZoneParser API
* Use the ZoneParser API directly in ReadRR
* Merge parseZoneHelper into ParseZone
* Make generate string building slightly more efficient
* Add SetDefaultTTL method to ZoneParser
This makes it possible for external consumers to implement ReadRR.
* Make $INCLUDE directive opt-in
The $INCLUDE directive opens a user controlled file and parses it as
a DNS zone file. The error messages may reveal portions of sensitive
files, such as:
/etc/passwd: dns: not a TTL: "root❌0:0:root:/root:/bin/bash" at line: 1:31
/etc/shadow: dns: not a TTL: "root:$6$<redacted>::0:99999:7:::" at line: 1:125
Both ParseZone and ReadRR are currently opt-in for backward
compatibility.
* Disable $INCLUDE support in ReadRR
ReadRR and NewRR are often passed untrusted input. At the same time,
$INCLUDE isn't really useful for ReadRR as it only ever returns the
first record.
This is a breaking change, but it currently represents a slight
security risk.
* Document the need to drain the ParseZone chan
* Cleanup the documentation of NewRR, ReadRR and ParseZone
* Document the ZoneParser API
* Deprecated the ParseZone function
* Add whitespace to ZoneParser.Next
* Remove prevName field from ZoneParser
This doesn't track anything meaningful as both zp.prevName and h.Name
are only ever set at the same point and to the same value.
* Use uint8 for ZoneParser.include field
It has a maximum value of 7 which easily fits within uint8.
This reduces the size of ZoneParser from 160 bytes to 152 bytes.
* Add setParseError helper to ZoneParser
* Surface $INCLUDE os.Open error in error message
* Rename ZoneParser.include field to includeDepth
* Make maximum $INCLUDE depth a const
* Add ParseZone and ZoneParser benchmarks
* Parse $GENERATE directive with a single ZoneParser
This should be more efficient than calling NewRR for each generated
record.
* Run go fmt on generate_test.go
* Add a benchmark for $GENERATE directives
* Use a custom reader for generate
This avoids the overhead and memory usage of building the zone string.
name old time/op new time/op delta
Generate-12 165µs ± 4% 157µs ± 2% -5.06% (p=0.000 n=25+25)
name old alloc/op new alloc/op delta
Generate-12 42.1kB ± 0% 31.8kB ± 0% -24.42% (p=0.000 n=20+23)
name old allocs/op new allocs/op delta
Generate-12 1.56k ± 0% 1.55k ± 0% -0.38% (p=0.000 n=25+25)
* Return correct ParseError from generateReader
The last commit made these regular errors while they had been
ParseErrors before.
* Return error message as string from modToPrintf
This is slightly simpler and they don't need to be errors.
* Skip setting includeDepth in generate
This sub parser isn't allowed to use $INCLUDE directives anyway.
Note: If generate is ever changed to allow $INCLUDE directives, then
this line must be added back. Without doing that, it would be
be possible to exceed maxIncludeDepth.
* Make generateReader errors sticky
ReadByte should not be called after an error has been returned, but
this is cheap insurance.
* Move file and lex fields to end of generateReader
These are only used for creating a ParseError and so are unlikely to be
accessed.
* Don't return offset with error in modToPrintf
Along for the ride, are some whitespace and style changes.
* Add whitespace to generate and simplify step
* Use a for loop instead of goto in generate
* Support $INCLUDE directives inside $GENERATE directives
This was previously supported and may be useful. This is now more
rigorous as the maximum include depth is respected and relative
$INCLUDE directives are now supported from within $GENERATE.
* Don't return any lexer tokens after read error
Without this, read errors are likely to be lost and become parse errors
of the remaining str. The $GENERATE code relies on surfacing errors from
the reader.
* Support $INCLUDE in NewRR and ReadRR
Removing $INCLUDE support from these is a breaking change and should
not be included in this pull request.
* Add test to ensure $GENERATE respects $INCLUDE support
* Unify TestZoneParserIncludeDisallowed with other tests
* Remove stray whitespace from TestGenerateSurfacesErrors
* Move ZoneParser SetX methods above Err method
* $GENERATE should not accept step of 0
If step is allowed to be 0, then generateReader (and the code it
replaced) will get stuck in an infinite loop.
This is a potential DOS vulnerability.
* Fix ReadRR comment for file argument
I missed this previosuly. The file argument is also used to
resolve relative $INCLUDE directives.
* Prevent test panics on nil error
* Rework ZoneParser.subNext
This is slightly cleaner and will close the underlying *os.File even
if an error occurs.
* Make ZoneParser.generate call subNext
This also moves the calls to setParseError into generate.
* Report errors when parsing rest of $GENERATE directive
* Report proper error location in $GENERATE directive
This makes error messages much clearer.
* Simplify modToPrintf func
Note: When width is 0, the leading 0 of the fmt string is now excluded.
This should not alter the formatting of numbers in anyway.
* Add comment explaining sub field
* Remove outdated error comment from generate
This commit is contained in:
parent
17c1bc6792
commit
274da7d3ef
|
@ -336,6 +336,11 @@ func (kl *klexer) Next() (lex, bool) {
|
|||
}
|
||||
}
|
||||
|
||||
if kl.readErr != nil && kl.readErr != io.EOF {
|
||||
// Don't return any tokens after a read error occurs.
|
||||
return lex{value: zEOF}, false
|
||||
}
|
||||
|
||||
if str.Len() > 0 {
|
||||
// Send remainder
|
||||
l.value = zValue
|
||||
|
|
305
generate.go
305
generate.go
|
@ -2,8 +2,8 @@ package dns
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
@ -18,154 +18,225 @@ import (
|
|||
// * rhs (rdata)
|
||||
// But we are lazy here, only the range is parsed *all* occurrences
|
||||
// of $ after that are interpreted.
|
||||
// Any error are returned as a string value, the empty string signals
|
||||
// "no error".
|
||||
func generate(l lex, c *zlexer, t chan *Token, o string) string {
|
||||
func (zp *ZoneParser) generate(l lex) (RR, bool) {
|
||||
token := l.token
|
||||
step := 1
|
||||
if i := strings.IndexAny(l.token, "/"); i != -1 {
|
||||
if i+1 == len(l.token) {
|
||||
return "bad step in $GENERATE range"
|
||||
if i := strings.IndexByte(token, '/'); i >= 0 {
|
||||
if i+1 == len(token) {
|
||||
return zp.setParseError("bad step in $GENERATE range", l)
|
||||
}
|
||||
if s, err := strconv.Atoi(l.token[i+1:]); err == nil {
|
||||
if s < 0 {
|
||||
return "bad step in $GENERATE range"
|
||||
}
|
||||
step = s
|
||||
} else {
|
||||
return "bad step in $GENERATE range"
|
||||
|
||||
s, err := strconv.Atoi(token[i+1:])
|
||||
if err != nil || s <= 0 {
|
||||
return zp.setParseError("bad step in $GENERATE range", l)
|
||||
}
|
||||
l.token = l.token[:i]
|
||||
|
||||
step = s
|
||||
token = token[:i]
|
||||
}
|
||||
sx := strings.SplitN(l.token, "-", 2)
|
||||
|
||||
sx := strings.SplitN(token, "-", 2)
|
||||
if len(sx) != 2 {
|
||||
return "bad start-stop in $GENERATE range"
|
||||
return zp.setParseError("bad start-stop in $GENERATE range", l)
|
||||
}
|
||||
|
||||
start, err := strconv.Atoi(sx[0])
|
||||
if err != nil {
|
||||
return "bad start in $GENERATE range"
|
||||
return zp.setParseError("bad start in $GENERATE range", l)
|
||||
}
|
||||
|
||||
end, err := strconv.Atoi(sx[1])
|
||||
if err != nil {
|
||||
return "bad stop in $GENERATE range"
|
||||
return zp.setParseError("bad stop in $GENERATE range", l)
|
||||
}
|
||||
if end < 0 || start < 0 || end < start {
|
||||
return "bad range in $GENERATE range"
|
||||
return zp.setParseError("bad range in $GENERATE range", l)
|
||||
}
|
||||
|
||||
c.Next() // _BLANK
|
||||
zp.c.Next() // _BLANK
|
||||
|
||||
// Create a complete new string, which we then parse again.
|
||||
s := ""
|
||||
BuildRR:
|
||||
l, _ = c.Next()
|
||||
if l.value != zNewline && l.value != zEOF {
|
||||
s += l.token
|
||||
goto BuildRR
|
||||
}
|
||||
for i := start; i <= end; i += step {
|
||||
var (
|
||||
escape bool
|
||||
dom bytes.Buffer
|
||||
mod string
|
||||
err error
|
||||
offset int
|
||||
)
|
||||
var s string
|
||||
for l, ok := zp.c.Next(); ok; l, ok = zp.c.Next() {
|
||||
if l.err {
|
||||
return zp.setParseError("bad data in $GENERATE directive", l)
|
||||
}
|
||||
if l.value == zNewline {
|
||||
break
|
||||
}
|
||||
|
||||
for j := 0; j < len(s); j++ { // No 'range' because we need to jump around
|
||||
switch s[j] {
|
||||
case '\\':
|
||||
if escape {
|
||||
dom.WriteByte('\\')
|
||||
escape = false
|
||||
continue
|
||||
}
|
||||
escape = true
|
||||
case '$':
|
||||
mod = "%d"
|
||||
offset = 0
|
||||
if escape {
|
||||
dom.WriteByte('$')
|
||||
escape = false
|
||||
continue
|
||||
}
|
||||
escape = false
|
||||
if j+1 >= len(s) { // End of the string
|
||||
dom.WriteString(fmt.Sprintf(mod, i+offset))
|
||||
continue
|
||||
} else {
|
||||
if s[j+1] == '$' {
|
||||
dom.WriteByte('$')
|
||||
j++
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Search for { and }
|
||||
if s[j+1] == '{' { // Modifier block
|
||||
sep := strings.Index(s[j+2:], "}")
|
||||
if sep == -1 {
|
||||
return "bad modifier in $GENERATE"
|
||||
}
|
||||
mod, offset, err = modToPrintf(s[j+2 : j+2+sep])
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
} else if start+offset < 0 || end+offset > 1<<31-1 {
|
||||
return "bad offset in $GENERATE"
|
||||
}
|
||||
j += 2 + sep // Jump to it
|
||||
}
|
||||
dom.WriteString(fmt.Sprintf(mod, i+offset))
|
||||
default:
|
||||
if escape { // Pretty useless here
|
||||
escape = false
|
||||
continue
|
||||
}
|
||||
dom.WriteByte(s[j])
|
||||
}
|
||||
}
|
||||
// Re-parse the RR and send it on the current channel t
|
||||
rx, err := NewRR("$ORIGIN " + o + "\n" + dom.String())
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
t <- &Token{RR: rx}
|
||||
// Its more efficient to first built the rrlist and then parse it in
|
||||
// one go! But is this a problem?
|
||||
s += l.token
|
||||
}
|
||||
|
||||
r := &generateReader{
|
||||
s: s,
|
||||
|
||||
cur: start,
|
||||
start: start,
|
||||
end: end,
|
||||
step: step,
|
||||
|
||||
file: zp.file,
|
||||
lex: &l,
|
||||
}
|
||||
zp.sub = NewZoneParser(r, zp.origin, zp.file)
|
||||
zp.sub.includeDepth, zp.sub.includeAllowed = zp.includeDepth, zp.includeAllowed
|
||||
zp.sub.SetDefaultTTL(defaultTtl)
|
||||
return zp.subNext()
|
||||
}
|
||||
|
||||
type generateReader struct {
|
||||
s string
|
||||
si int
|
||||
|
||||
cur int
|
||||
start int
|
||||
end int
|
||||
step int
|
||||
|
||||
mod bytes.Buffer
|
||||
|
||||
escape bool
|
||||
|
||||
eof bool
|
||||
|
||||
file string
|
||||
lex *lex
|
||||
}
|
||||
|
||||
func (r *generateReader) parseError(msg string, end int) *ParseError {
|
||||
r.eof = true // Make errors sticky.
|
||||
|
||||
l := *r.lex
|
||||
l.token = r.s[r.si-1 : end]
|
||||
l.column += r.si // l.column starts one zBLANK before r.s
|
||||
|
||||
return &ParseError{r.file, msg, l}
|
||||
}
|
||||
|
||||
func (r *generateReader) Read(p []byte) (int, error) {
|
||||
// NewZLexer, through NewZoneParser, should use ReadByte and
|
||||
// not end up here.
|
||||
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (r *generateReader) ReadByte() (byte, error) {
|
||||
if r.eof {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if r.mod.Len() > 0 {
|
||||
return r.mod.ReadByte()
|
||||
}
|
||||
|
||||
if r.si >= len(r.s) {
|
||||
r.si = 0
|
||||
r.cur += r.step
|
||||
|
||||
r.eof = r.cur > r.end || r.cur < 0
|
||||
return '\n', nil
|
||||
}
|
||||
|
||||
si := r.si
|
||||
r.si++
|
||||
|
||||
switch r.s[si] {
|
||||
case '\\':
|
||||
if r.escape {
|
||||
r.escape = false
|
||||
return '\\', nil
|
||||
}
|
||||
|
||||
r.escape = true
|
||||
return r.ReadByte()
|
||||
case '$':
|
||||
if r.escape {
|
||||
r.escape = false
|
||||
return '$', nil
|
||||
}
|
||||
|
||||
mod := "%d"
|
||||
|
||||
if si >= len(r.s)-1 {
|
||||
// End of the string
|
||||
fmt.Fprintf(&r.mod, mod, r.cur)
|
||||
return r.mod.ReadByte()
|
||||
}
|
||||
|
||||
if r.s[si+1] == '$' {
|
||||
r.si++
|
||||
return '$', nil
|
||||
}
|
||||
|
||||
var offset int
|
||||
|
||||
// Search for { and }
|
||||
if r.s[si+1] == '{' {
|
||||
// Modifier block
|
||||
sep := strings.Index(r.s[si+2:], "}")
|
||||
if sep < 0 {
|
||||
return 0, r.parseError("bad modifier in $GENERATE", len(r.s))
|
||||
}
|
||||
|
||||
var errMsg string
|
||||
mod, offset, errMsg = modToPrintf(r.s[si+2 : si+2+sep])
|
||||
if errMsg != "" {
|
||||
return 0, r.parseError(errMsg, si+3+sep)
|
||||
}
|
||||
if r.start+offset < 0 || r.end+offset > 1<<31-1 {
|
||||
return 0, r.parseError("bad offset in $GENERATE", si+3+sep)
|
||||
}
|
||||
|
||||
r.si += 2 + sep // Jump to it
|
||||
}
|
||||
|
||||
fmt.Fprintf(&r.mod, mod, r.cur+offset)
|
||||
return r.mod.ReadByte()
|
||||
default:
|
||||
if r.escape { // Pretty useless here
|
||||
r.escape = false
|
||||
return r.ReadByte()
|
||||
}
|
||||
|
||||
return r.s[si], nil
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Convert a $GENERATE modifier 0,0,d to something Printf can deal with.
|
||||
func modToPrintf(s string) (string, int, error) {
|
||||
xs := strings.Split(s, ",")
|
||||
|
||||
func modToPrintf(s string) (string, int, string) {
|
||||
// Modifier is { offset [ ,width [ ,base ] ] } - provide default
|
||||
// values for optional width and type, if necessary.
|
||||
switch len(xs) {
|
||||
var offStr, widthStr, base string
|
||||
switch xs := strings.Split(s, ","); len(xs) {
|
||||
case 1:
|
||||
xs = append(xs, "0", "d")
|
||||
offStr, widthStr, base = xs[0], "0", "d"
|
||||
case 2:
|
||||
xs = append(xs, "d")
|
||||
offStr, widthStr, base = xs[0], xs[1], "d"
|
||||
case 3:
|
||||
offStr, widthStr, base = xs[0], xs[1], xs[2]
|
||||
default:
|
||||
return "", 0, errors.New("bad modifier in $GENERATE")
|
||||
return "", 0, "bad modifier in $GENERATE"
|
||||
}
|
||||
|
||||
// xs[0] is offset, xs[1] is width, xs[2] is base
|
||||
if xs[2] != "o" && xs[2] != "d" && xs[2] != "x" && xs[2] != "X" {
|
||||
return "", 0, errors.New("bad base in $GENERATE")
|
||||
switch base {
|
||||
case "o", "d", "x", "X":
|
||||
default:
|
||||
return "", 0, "bad base in $GENERATE"
|
||||
}
|
||||
offset, err := strconv.Atoi(xs[0])
|
||||
|
||||
offset, err := strconv.Atoi(offStr)
|
||||
if err != nil {
|
||||
return "", 0, errors.New("bad offset in $GENERATE")
|
||||
return "", 0, "bad offset in $GENERATE"
|
||||
}
|
||||
width, err := strconv.Atoi(xs[1])
|
||||
if err != nil || width > 255 {
|
||||
return "", offset, errors.New("bad width in $GENERATE")
|
||||
|
||||
width, err := strconv.Atoi(widthStr)
|
||||
if err != nil || width < 0 || width > 255 {
|
||||
return "", 0, "bad width in $GENERATE"
|
||||
}
|
||||
switch {
|
||||
case width < 0:
|
||||
return "", offset, errors.New("bad width in $GENERATE")
|
||||
case width == 0:
|
||||
return "%" + xs[1] + xs[2], offset, nil
|
||||
|
||||
if width == 0 {
|
||||
return "%" + base, offset, ""
|
||||
}
|
||||
return "%0" + xs[1] + xs[2], offset, nil
|
||||
|
||||
return "%0" + widthStr + base, offset, ""
|
||||
}
|
||||
|
|
176
generate_test.go
176
generate_test.go
|
@ -1,29 +1,66 @@
|
|||
package dns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateRangeGuard(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "dns")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create tmpdir for test: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
for i := 0; i <= 1; i++ {
|
||||
path := filepath.Join(tmpdir, fmt.Sprintf("%04d.conf", i))
|
||||
data := []byte(fmt.Sprintf("dhcp-%04d A 10.0.0.%d", i, i))
|
||||
|
||||
if err := ioutil.WriteFile(path, data, 0644); err != nil {
|
||||
t.Fatalf("could not create tmpfile for test: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var tests = [...]struct {
|
||||
zone string
|
||||
fail bool
|
||||
}{{
|
||||
`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
}{
|
||||
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-1 dhcp-${0,4,d} A 10.0.0.$
|
||||
`, false},{
|
||||
`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
`, false},
|
||||
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-1 dhcp-${0,0,x} A 10.0.0.$
|
||||
`, false},
|
||||
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 128-129 dhcp-${-128,4,d} A 10.0.0.$
|
||||
`, false},{
|
||||
`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
`, false},
|
||||
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 128-129 dhcp-${-129,4,d} A 10.0.0.$
|
||||
`, true},{
|
||||
`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
`, true},
|
||||
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-2 dhcp-${2147483647,4,d} A 10.0.0.$
|
||||
`, true},{
|
||||
`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
`, true},
|
||||
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-1 dhcp-${2147483646,4,d} A 10.0.0.$
|
||||
`, false},
|
||||
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-1/step dhcp-${0,4,d} A 10.0.0.$
|
||||
`, true},
|
||||
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-1/ dhcp-${0,4,d} A 10.0.0.$
|
||||
`, true},
|
||||
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-10/2 dhcp-${0,4,d} A 10.0.0.$
|
||||
`, false},
|
||||
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-1/0 dhcp-${0,4,d} A 10.0.0.$
|
||||
`, true},
|
||||
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-1 $$INCLUDE ` + tmpdir + string(filepath.Separator) + `${0,4,d}.conf
|
||||
`, false},
|
||||
}
|
||||
Outer:
|
||||
|
@ -42,6 +79,80 @@ Outer:
|
|||
}
|
||||
}
|
||||
|
||||
func TestGenerateIncludeDepth(t *testing.T) {
|
||||
tmpfile, err := ioutil.TempFile("", "dns")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create tmpfile for test: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
zone := `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-1 $$INCLUDE ` + tmpfile.Name() + `
|
||||
`
|
||||
if _, err := tmpfile.WriteString(zone); err != nil {
|
||||
t.Fatalf("could not write to tmpfile for test: %v", err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatalf("could not close tmpfile for test: %v", err)
|
||||
}
|
||||
|
||||
zp := NewZoneParser(strings.NewReader(zone), ".", tmpfile.Name())
|
||||
zp.SetIncludeAllowed(true)
|
||||
|
||||
for _, ok := zp.Next(); ok; _, ok = zp.Next() {
|
||||
}
|
||||
|
||||
const expected = "too deeply nested $INCLUDE"
|
||||
if err := zp.Err(); err == nil || !strings.Contains(err.Error(), expected) {
|
||||
t.Errorf("expected error to include %q, got %v", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateIncludeDisallowed(t *testing.T) {
|
||||
const zone = `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-1 $$INCLUDE test.conf
|
||||
`
|
||||
zp := NewZoneParser(strings.NewReader(zone), ".", "")
|
||||
|
||||
for _, ok := zp.Next(); ok; _, ok = zp.Next() {
|
||||
}
|
||||
|
||||
const expected = "$INCLUDE directive not allowed"
|
||||
if err := zp.Err(); err == nil || !strings.Contains(err.Error(), expected) {
|
||||
t.Errorf("expected error to include %q, got %v", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSurfacesErrors(t *testing.T) {
|
||||
const zone = `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-1 dhcp-${0,4,dd} A 10.0.0.$
|
||||
`
|
||||
zp := NewZoneParser(strings.NewReader(zone), ".", "test")
|
||||
|
||||
for _, ok := zp.Next(); ok; _, ok = zp.Next() {
|
||||
}
|
||||
|
||||
const expected = `test: dns: bad base in $GENERATE: "${0,4,dd}" at line: 2:20`
|
||||
if err := zp.Err(); err == nil || err.Error() != expected {
|
||||
t.Errorf("expected specific error, wanted %q, got %v", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSurfacesLexerErrors(t *testing.T) {
|
||||
const zone = `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 0-1 dhcp-${0,4,d} A 10.0.0.$ )
|
||||
`
|
||||
zp := NewZoneParser(strings.NewReader(zone), ".", "test")
|
||||
|
||||
for _, ok := zp.Next(); ok; _, ok = zp.Next() {
|
||||
}
|
||||
|
||||
const expected = `test: dns: bad data in $GENERATE directive: "extra closing brace" at line: 2:40`
|
||||
if err := zp.Err(); err == nil || err.Error() != expected {
|
||||
t.Errorf("expected specific error, wanted %q, got %v", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateModToPrintf(t *testing.T) {
|
||||
tests := []struct {
|
||||
mod string
|
||||
|
@ -49,26 +160,26 @@ func TestGenerateModToPrintf(t *testing.T) {
|
|||
wantOffset int
|
||||
wantErr bool
|
||||
}{
|
||||
{"0,0,d", "%0d", 0, false},
|
||||
{"0,0", "%0d", 0, false},
|
||||
{"0", "%0d", 0, false},
|
||||
{"0,0,d", "%d", 0, false},
|
||||
{"0,0", "%d", 0, false},
|
||||
{"0", "%d", 0, false},
|
||||
{"3,2,d", "%02d", 3, false},
|
||||
{"3,2", "%02d", 3, false},
|
||||
{"3", "%0d", 3, false},
|
||||
{"0,0,o", "%0o", 0, false},
|
||||
{"0,0,x", "%0x", 0, false},
|
||||
{"0,0,X", "%0X", 0, false},
|
||||
{"3", "%d", 3, false},
|
||||
{"0,0,o", "%o", 0, false},
|
||||
{"0,0,x", "%x", 0, false},
|
||||
{"0,0,X", "%X", 0, false},
|
||||
{"0,0,z", "", 0, true},
|
||||
{"0,0,0,d", "", 0, true},
|
||||
{"-100,0,d", "%0d", -100, false},
|
||||
{"-100,0,d", "%d", -100, false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
gotFmt, gotOffset, err := modToPrintf(test.mod)
|
||||
gotFmt, gotOffset, errMsg := modToPrintf(test.mod)
|
||||
switch {
|
||||
case err != nil && !test.wantErr:
|
||||
t.Errorf("modToPrintf(%q) - expected nil-error, but got %v", test.mod, err)
|
||||
case err == nil && test.wantErr:
|
||||
t.Errorf("modToPrintf(%q) - expected error, but got nil-error", test.mod)
|
||||
case errMsg != "" && !test.wantErr:
|
||||
t.Errorf("modToPrintf(%q) - expected empty-error, but got %v", test.mod, errMsg)
|
||||
case errMsg == "" && test.wantErr:
|
||||
t.Errorf("modToPrintf(%q) - expected error, but got empty-error", test.mod)
|
||||
case gotFmt != test.wantFmt:
|
||||
t.Errorf("modToPrintf(%q) - expected format %q, but got %q", test.mod, test.wantFmt, gotFmt)
|
||||
case gotOffset != test.wantOffset:
|
||||
|
@ -76,3 +187,20 @@ func TestGenerateModToPrintf(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGenerate(b *testing.B) {
|
||||
const zone = `@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||
$GENERATE 32-158 dhcp-${-32,4,d} A 10.0.0.$
|
||||
`
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
zp := NewZoneParser(strings.NewReader(zone), ".", "")
|
||||
|
||||
for _, ok := zp.Next(); ok; _, ok = zp.Next() {
|
||||
}
|
||||
|
||||
if err := zp.Err(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
529
scan.go
529
scan.go
|
@ -12,6 +12,10 @@ import (
|
|||
|
||||
const maxTok = 2048 // Largest token we can return.
|
||||
|
||||
// The maximum depth of $INCLUDE directives supported by the
|
||||
// ZoneParser API.
|
||||
const maxIncludeDepth = 7
|
||||
|
||||
// Tokinize a RFC 1035 zone file. The tokenizer will normalize it:
|
||||
// * Add ownernames if they are left blank;
|
||||
// * Suppress sequences of spaces;
|
||||
|
@ -101,10 +105,14 @@ type ttlState struct {
|
|||
}
|
||||
|
||||
// NewRR reads the RR contained in the string s. Only the first RR is
|
||||
// returned. If s contains no RR, return nil with no error. The class
|
||||
// defaults to IN and TTL defaults to 3600. The full zone file syntax
|
||||
// like $TTL, $ORIGIN, etc. is supported. All fields of the returned
|
||||
// RR are set, except RR.Header().Rdlength which is set to 0.
|
||||
// returned. If s contains no records, NewRR will return nil with no
|
||||
// error.
|
||||
//
|
||||
// The class defaults to IN and TTL defaults to 3600. The full zone
|
||||
// file syntax like $TTL, $ORIGIN, etc. is supported.
|
||||
//
|
||||
// All fields of the returned RR are set, except RR.Header().Rdlength
|
||||
// which is set to 0.
|
||||
func NewRR(s string) (RR, error) {
|
||||
if len(s) > 0 && s[len(s)-1] != '\n' { // We need a closing newline
|
||||
return ReadRR(strings.NewReader(s+"\n"), "")
|
||||
|
@ -112,28 +120,31 @@ func NewRR(s string) (RR, error) {
|
|||
return ReadRR(strings.NewReader(s), "")
|
||||
}
|
||||
|
||||
// ReadRR reads the RR contained in q.
|
||||
// ReadRR reads the RR contained in r.
|
||||
//
|
||||
// The string file is used in error reporting and to resolve relative
|
||||
// $INCLUDE directives.
|
||||
//
|
||||
// See NewRR for more documentation.
|
||||
func ReadRR(q io.Reader, filename string) (RR, error) {
|
||||
defttl := &ttlState{defaultTtl, false}
|
||||
r := <-parseZoneHelper(q, ".", filename, defttl, 1)
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if r.Error != nil {
|
||||
return nil, r.Error
|
||||
}
|
||||
return r.RR, nil
|
||||
func ReadRR(r io.Reader, file string) (RR, error) {
|
||||
zp := NewZoneParser(r, ".", file)
|
||||
zp.SetDefaultTTL(defaultTtl)
|
||||
zp.SetIncludeAllowed(true)
|
||||
rr, _ := zp.Next()
|
||||
return rr, zp.Err()
|
||||
}
|
||||
|
||||
// ParseZone reads a RFC 1035 style zonefile from r. It returns *Tokens on the
|
||||
// returned channel, each consisting of either a parsed RR and optional comment
|
||||
// or a nil RR and an error. The string file is only used
|
||||
// in error reporting. The string origin is used as the initial origin, as
|
||||
// if the file would start with an $ORIGIN directive.
|
||||
// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are supported.
|
||||
// The channel t is closed by ParseZone when the end of r is reached.
|
||||
// ParseZone reads a RFC 1035 style zonefile from r. It returns
|
||||
// *Tokens on the returned channel, each consisting of either a
|
||||
// parsed RR and optional comment or a nil RR and an error. The
|
||||
// channel is closed by ParseZone when the end of r is reached.
|
||||
//
|
||||
// The string file is used in error reporting and to resolve relative
|
||||
// $INCLUDE directives. The string origin is used as the initial
|
||||
// origin, as if the file would start with an $ORIGIN directive.
|
||||
//
|
||||
// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are all
|
||||
// supported.
|
||||
//
|
||||
// Basic usage pattern when reading from a string (z) containing the
|
||||
// zone data:
|
||||
|
@ -146,78 +157,246 @@ func ReadRR(q io.Reader, filename string) (RR, error) {
|
|||
// }
|
||||
// }
|
||||
//
|
||||
// Comments specified after an RR (and on the same line!) are returned too:
|
||||
// Comments specified after an RR (and on the same line!) are
|
||||
// returned too:
|
||||
//
|
||||
// foo. IN A 10.0.0.1 ; this is a comment
|
||||
//
|
||||
// The text "; this is comment" is returned in Token.Comment. Comments inside the
|
||||
// RR are returned concatenated along with the RR. Comments on a line by themselves
|
||||
// are discarded.
|
||||
// The text "; this is comment" is returned in Token.Comment.
|
||||
// Comments inside the RR are returned concatenated along with the
|
||||
// RR. Comments on a line by themselves are discarded.
|
||||
//
|
||||
// To prevent memory leaks it is important to always fully drain the
|
||||
// returned channel. If an error occurs, it will always be the last
|
||||
// Token sent on the channel.
|
||||
//
|
||||
// Deprecated: New users should prefer the ZoneParser API.
|
||||
func ParseZone(r io.Reader, origin, file string) chan *Token {
|
||||
return parseZoneHelper(r, origin, file, nil, 10000)
|
||||
}
|
||||
|
||||
func parseZoneHelper(r io.Reader, origin, file string, defttl *ttlState, chansize int) chan *Token {
|
||||
t := make(chan *Token, chansize)
|
||||
go parseZone(r, origin, file, defttl, t, 0)
|
||||
t := make(chan *Token, 10000)
|
||||
go parseZone(r, origin, file, t)
|
||||
return t
|
||||
}
|
||||
|
||||
func parseZone(r io.Reader, origin, f string, defttl *ttlState, t chan *Token, include int) {
|
||||
defer func() {
|
||||
if include == 0 {
|
||||
close(t)
|
||||
func parseZone(r io.Reader, origin, file string, t chan *Token) {
|
||||
defer close(t)
|
||||
|
||||
zp := NewZoneParser(r, origin, file)
|
||||
zp.SetIncludeAllowed(true)
|
||||
|
||||
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
|
||||
t <- &Token{RR: rr, Comment: zp.Comment()}
|
||||
}
|
||||
|
||||
if err := zp.Err(); err != nil {
|
||||
pe, ok := err.(*ParseError)
|
||||
if !ok {
|
||||
pe = &ParseError{file: file, err: err.Error()}
|
||||
}
|
||||
}()
|
||||
|
||||
c := newZLexer(r)
|
||||
t <- &Token{Error: pe}
|
||||
}
|
||||
}
|
||||
|
||||
// 6 possible beginnings of a line, _ is a space
|
||||
// 0. zRRTYPE -> all omitted until the rrtype
|
||||
// 1. zOwner _ zRrtype -> class/ttl omitted
|
||||
// 2. zOwner _ zString _ zRrtype -> class omitted
|
||||
// 3. zOwner _ zString _ zClass _ zRrtype -> ttl/class
|
||||
// 4. zOwner _ zClass _ zRrtype -> ttl omitted
|
||||
// 5. zOwner _ zClass _ zString _ zRrtype -> class/ttl (reversed)
|
||||
// After detecting these, we know the zRrtype so we can jump to functions
|
||||
// handling the rdata for each of these types.
|
||||
// ZoneParser is a parser for an RFC 1035 style zonefile.
|
||||
//
|
||||
// Each parsed RR in the zone is returned sequentially from Next. An
|
||||
// optional comment can be retrieved with Comment.
|
||||
//
|
||||
// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are all
|
||||
// supported. Although $INCLUDE is disabled by default.
|
||||
//
|
||||
// Basic usage pattern when reading from a string (z) containing the
|
||||
// zone data:
|
||||
//
|
||||
// zp := NewZoneParser(strings.NewReader(z), "", "")
|
||||
//
|
||||
// for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
|
||||
// // Do something with rr
|
||||
// }
|
||||
//
|
||||
// if err := zp.Err(); err != nil {
|
||||
// // log.Println(err)
|
||||
// }
|
||||
//
|
||||
// Comments specified after an RR (and on the same line!) are
|
||||
// returned too:
|
||||
//
|
||||
// foo. IN A 10.0.0.1 ; this is a comment
|
||||
//
|
||||
// The text "; this is comment" is returned from Comment. Comments inside
|
||||
// the RR are returned concatenated along with the RR. Comments on a line
|
||||
// by themselves are discarded.
|
||||
type ZoneParser struct {
|
||||
c *zlexer
|
||||
|
||||
parseErr *ParseError
|
||||
|
||||
origin string
|
||||
file string
|
||||
|
||||
defttl *ttlState
|
||||
|
||||
h RR_Header
|
||||
|
||||
// sub is used to parse $INCLUDE files and $GENERATE directives.
|
||||
// Next, by calling subNext, forwards the resulting RRs from this
|
||||
// sub parser to the calling code.
|
||||
sub *ZoneParser
|
||||
osFile *os.File
|
||||
|
||||
com string
|
||||
|
||||
includeDepth uint8
|
||||
|
||||
includeAllowed bool
|
||||
}
|
||||
|
||||
// NewZoneParser returns an RFC 1035 style zonefile parser that reads
|
||||
// from r.
|
||||
//
|
||||
// The string file is used in error reporting and to resolve relative
|
||||
// $INCLUDE directives. The string origin is used as the initial
|
||||
// origin, as if the file would start with an $ORIGIN directive.
|
||||
func NewZoneParser(r io.Reader, origin, file string) *ZoneParser {
|
||||
var pe *ParseError
|
||||
if origin != "" {
|
||||
origin = Fqdn(origin)
|
||||
if _, ok := IsDomainName(origin); !ok {
|
||||
t <- &Token{Error: &ParseError{f, "bad initial origin name", lex{}}}
|
||||
return
|
||||
pe = &ParseError{file, "bad initial origin name", lex{}}
|
||||
}
|
||||
}
|
||||
|
||||
st := zExpectOwnerDir // initial state
|
||||
var h RR_Header
|
||||
var prevName string
|
||||
for l, ok := c.Next(); ok; l, ok = c.Next() {
|
||||
// Lexer spotted an error already
|
||||
if l.err {
|
||||
t <- &Token{Error: &ParseError{f, l.token, l}}
|
||||
return
|
||||
return &ZoneParser{
|
||||
c: newZLexer(r),
|
||||
|
||||
parseErr: pe,
|
||||
|
||||
origin: origin,
|
||||
file: file,
|
||||
}
|
||||
}
|
||||
|
||||
// SetDefaultTTL sets the parsers default TTL to ttl.
|
||||
func (zp *ZoneParser) SetDefaultTTL(ttl uint32) {
|
||||
zp.defttl = &ttlState{ttl, false}
|
||||
}
|
||||
|
||||
// SetIncludeAllowed controls whether $INCLUDE directives are
|
||||
// allowed. $INCLUDE directives are not supported by default.
|
||||
//
|
||||
// The $INCLUDE directive will open and read from a user controlled
|
||||
// file on the system. Even if the file is not a valid zonefile, the
|
||||
// contents of the file may be revealed in error messages, such as:
|
||||
//
|
||||
// /etc/passwd: dns: not a TTL: "root:x:0:0:root:/root:/bin/bash" at line: 1:31
|
||||
// /etc/shadow: dns: not a TTL: "root:$6$<redacted>::0:99999:7:::" at line: 1:125
|
||||
func (zp *ZoneParser) SetIncludeAllowed(v bool) {
|
||||
zp.includeAllowed = v
|
||||
}
|
||||
|
||||
// Err returns the first non-EOF error that was encountered by the
|
||||
// ZoneParser.
|
||||
func (zp *ZoneParser) Err() error {
|
||||
if zp.parseErr != nil {
|
||||
return zp.parseErr
|
||||
}
|
||||
|
||||
if zp.sub != nil {
|
||||
if err := zp.sub.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return zp.c.Err()
|
||||
}
|
||||
|
||||
func (zp *ZoneParser) setParseError(err string, l lex) (RR, bool) {
|
||||
zp.parseErr = &ParseError{zp.file, err, l}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Comment returns an optional text comment that occurred alongside
|
||||
// the RR.
|
||||
func (zp *ZoneParser) Comment() string {
|
||||
return zp.com
|
||||
}
|
||||
|
||||
func (zp *ZoneParser) subNext() (RR, bool) {
|
||||
if rr, ok := zp.sub.Next(); ok {
|
||||
zp.com = zp.sub.com
|
||||
return rr, true
|
||||
}
|
||||
|
||||
if zp.sub.osFile != nil {
|
||||
zp.sub.osFile.Close()
|
||||
zp.sub.osFile = nil
|
||||
}
|
||||
|
||||
if zp.sub.Err() != nil {
|
||||
// We have errors to surface.
|
||||
return nil, false
|
||||
}
|
||||
|
||||
zp.sub = nil
|
||||
return zp.Next()
|
||||
}
|
||||
|
||||
// Next advances the parser to the next RR in the zonefile and
|
||||
// returns the (RR, true). It will return (nil, false) when the
|
||||
// parsing stops, either by reaching the end of the input or an
|
||||
// error. After Next returns (nil, false), the Err method will return
|
||||
// any error that occurred during parsing.
|
||||
func (zp *ZoneParser) Next() (RR, bool) {
|
||||
zp.com = ""
|
||||
|
||||
if zp.parseErr != nil {
|
||||
return nil, false
|
||||
}
|
||||
if zp.sub != nil {
|
||||
return zp.subNext()
|
||||
}
|
||||
|
||||
// 6 possible beginnings of a line (_ is a space):
|
||||
//
|
||||
// 0. zRRTYPE -> all omitted until the rrtype
|
||||
// 1. zOwner _ zRrtype -> class/ttl omitted
|
||||
// 2. zOwner _ zString _ zRrtype -> class omitted
|
||||
// 3. zOwner _ zString _ zClass _ zRrtype -> ttl/class
|
||||
// 4. zOwner _ zClass _ zRrtype -> ttl omitted
|
||||
// 5. zOwner _ zClass _ zString _ zRrtype -> class/ttl (reversed)
|
||||
//
|
||||
// After detecting these, we know the zRrtype so we can jump to functions
|
||||
// handling the rdata for each of these types.
|
||||
|
||||
st := zExpectOwnerDir // initial state
|
||||
h := &zp.h
|
||||
|
||||
for l, ok := zp.c.Next(); ok; l, ok = zp.c.Next() {
|
||||
// zlexer spotted an error already
|
||||
if l.err {
|
||||
return zp.setParseError(l.token, l)
|
||||
}
|
||||
|
||||
switch st {
|
||||
case zExpectOwnerDir:
|
||||
// We can also expect a directive, like $TTL or $ORIGIN
|
||||
if defttl != nil {
|
||||
h.Ttl = defttl.ttl
|
||||
if zp.defttl != nil {
|
||||
h.Ttl = zp.defttl.ttl
|
||||
}
|
||||
|
||||
h.Class = ClassINET
|
||||
|
||||
switch l.value {
|
||||
case zNewline:
|
||||
st = zExpectOwnerDir
|
||||
case zOwner:
|
||||
h.Name = l.token
|
||||
name, ok := toAbsoluteName(l.token, origin)
|
||||
name, ok := toAbsoluteName(l.token, zp.origin)
|
||||
if !ok {
|
||||
t <- &Token{Error: &ParseError{f, "bad owner name", l}}
|
||||
return
|
||||
return zp.setParseError("bad owner name", l)
|
||||
}
|
||||
|
||||
h.Name = name
|
||||
prevName = h.Name
|
||||
|
||||
st = zExpectOwnerBl
|
||||
case zDirTTL:
|
||||
st = zExpectDirTTLBl
|
||||
|
@ -228,12 +407,12 @@ func parseZone(r io.Reader, origin, f string, defttl *ttlState, t chan *Token, i
|
|||
case zDirGenerate:
|
||||
st = zExpectDirGenerateBl
|
||||
case zRrtpe:
|
||||
h.Name = prevName
|
||||
h.Rrtype = l.torc
|
||||
|
||||
st = zExpectRdata
|
||||
case zClass:
|
||||
h.Name = prevName
|
||||
h.Class = l.torc
|
||||
|
||||
st = zExpectAnyNoClassBl
|
||||
case zBlank:
|
||||
// Discard, can happen when there is nothing on the
|
||||
|
@ -241,239 +420,254 @@ func parseZone(r io.Reader, origin, f string, defttl *ttlState, t chan *Token, i
|
|||
case zString:
|
||||
ttl, ok := stringToTTL(l.token)
|
||||
if !ok {
|
||||
t <- &Token{Error: &ParseError{f, "not a TTL", l}}
|
||||
return
|
||||
return zp.setParseError("not a TTL", l)
|
||||
}
|
||||
h.Ttl = ttl
|
||||
if defttl == nil || !defttl.isByDirective {
|
||||
defttl = &ttlState{ttl, false}
|
||||
}
|
||||
st = zExpectAnyNoTTLBl
|
||||
|
||||
h.Ttl = ttl
|
||||
|
||||
if zp.defttl == nil || !zp.defttl.isByDirective {
|
||||
zp.defttl = &ttlState{ttl, false}
|
||||
}
|
||||
|
||||
st = zExpectAnyNoTTLBl
|
||||
default:
|
||||
t <- &Token{Error: &ParseError{f, "syntax error at beginning", l}}
|
||||
return
|
||||
return zp.setParseError("syntax error at beginning", l)
|
||||
}
|
||||
case zExpectDirIncludeBl:
|
||||
if l.value != zBlank {
|
||||
t <- &Token{Error: &ParseError{f, "no blank after $INCLUDE-directive", l}}
|
||||
return
|
||||
return zp.setParseError("no blank after $INCLUDE-directive", l)
|
||||
}
|
||||
|
||||
st = zExpectDirInclude
|
||||
case zExpectDirInclude:
|
||||
if l.value != zString {
|
||||
t <- &Token{Error: &ParseError{f, "expecting $INCLUDE value, not this...", l}}
|
||||
return
|
||||
return zp.setParseError("expecting $INCLUDE value, not this...", l)
|
||||
}
|
||||
neworigin := origin // There may be optionally a new origin set after the filename, if not use current one
|
||||
switch l, _ := c.Next(); l.value {
|
||||
|
||||
neworigin := zp.origin // There may be optionally a new origin set after the filename, if not use current one
|
||||
switch l, _ := zp.c.Next(); l.value {
|
||||
case zBlank:
|
||||
l, _ := c.Next()
|
||||
l, _ := zp.c.Next()
|
||||
if l.value == zString {
|
||||
name, ok := toAbsoluteName(l.token, origin)
|
||||
name, ok := toAbsoluteName(l.token, zp.origin)
|
||||
if !ok {
|
||||
t <- &Token{Error: &ParseError{f, "bad origin name", l}}
|
||||
return
|
||||
return zp.setParseError("bad origin name", l)
|
||||
}
|
||||
|
||||
neworigin = name
|
||||
}
|
||||
case zNewline, zEOF:
|
||||
// Ok
|
||||
default:
|
||||
t <- &Token{Error: &ParseError{f, "garbage after $INCLUDE", l}}
|
||||
return
|
||||
return zp.setParseError("garbage after $INCLUDE", l)
|
||||
}
|
||||
|
||||
if !zp.includeAllowed {
|
||||
return zp.setParseError("$INCLUDE directive not allowed", l)
|
||||
}
|
||||
if zp.includeDepth >= maxIncludeDepth {
|
||||
return zp.setParseError("too deeply nested $INCLUDE", l)
|
||||
}
|
||||
|
||||
// Start with the new file
|
||||
includePath := l.token
|
||||
if !filepath.IsAbs(includePath) {
|
||||
includePath = filepath.Join(filepath.Dir(f), includePath)
|
||||
includePath = filepath.Join(filepath.Dir(zp.file), includePath)
|
||||
}
|
||||
|
||||
r1, e1 := os.Open(includePath)
|
||||
if e1 != nil {
|
||||
msg := fmt.Sprintf("failed to open `%s'", l.token)
|
||||
var as string
|
||||
if !filepath.IsAbs(l.token) {
|
||||
msg += fmt.Sprintf(" as `%s'", includePath)
|
||||
as = fmt.Sprintf(" as `%s'", includePath)
|
||||
}
|
||||
t <- &Token{Error: &ParseError{f, msg, l}}
|
||||
return
|
||||
|
||||
msg := fmt.Sprintf("failed to open `%s'%s: %v", l.token, as, e1)
|
||||
return zp.setParseError(msg, l)
|
||||
}
|
||||
if include+1 > 7 {
|
||||
t <- &Token{Error: &ParseError{f, "too deeply nested $INCLUDE", l}}
|
||||
return
|
||||
}
|
||||
parseZone(r1, neworigin, includePath, defttl, t, include+1)
|
||||
st = zExpectOwnerDir
|
||||
|
||||
zp.sub = NewZoneParser(r1, neworigin, includePath)
|
||||
zp.sub.defttl, zp.sub.includeDepth, zp.sub.osFile = zp.defttl, zp.includeDepth+1, r1
|
||||
zp.sub.SetIncludeAllowed(true)
|
||||
return zp.subNext()
|
||||
case zExpectDirTTLBl:
|
||||
if l.value != zBlank {
|
||||
t <- &Token{Error: &ParseError{f, "no blank after $TTL-directive", l}}
|
||||
return
|
||||
return zp.setParseError("no blank after $TTL-directive", l)
|
||||
}
|
||||
|
||||
st = zExpectDirTTL
|
||||
case zExpectDirTTL:
|
||||
if l.value != zString {
|
||||
t <- &Token{Error: &ParseError{f, "expecting $TTL value, not this...", l}}
|
||||
return
|
||||
return zp.setParseError("expecting $TTL value, not this...", l)
|
||||
}
|
||||
if e, _ := slurpRemainder(c, f); e != nil {
|
||||
t <- &Token{Error: e}
|
||||
return
|
||||
|
||||
if e, _ := slurpRemainder(zp.c, zp.file); e != nil {
|
||||
zp.parseErr = e
|
||||
return nil, false
|
||||
}
|
||||
|
||||
ttl, ok := stringToTTL(l.token)
|
||||
if !ok {
|
||||
t <- &Token{Error: &ParseError{f, "expecting $TTL value, not this...", l}}
|
||||
return
|
||||
return zp.setParseError("expecting $TTL value, not this...", l)
|
||||
}
|
||||
defttl = &ttlState{ttl, true}
|
||||
|
||||
zp.defttl = &ttlState{ttl, true}
|
||||
|
||||
st = zExpectOwnerDir
|
||||
case zExpectDirOriginBl:
|
||||
if l.value != zBlank {
|
||||
t <- &Token{Error: &ParseError{f, "no blank after $ORIGIN-directive", l}}
|
||||
return
|
||||
return zp.setParseError("no blank after $ORIGIN-directive", l)
|
||||
}
|
||||
|
||||
st = zExpectDirOrigin
|
||||
case zExpectDirOrigin:
|
||||
if l.value != zString {
|
||||
t <- &Token{Error: &ParseError{f, "expecting $ORIGIN value, not this...", l}}
|
||||
return
|
||||
return zp.setParseError("expecting $ORIGIN value, not this...", l)
|
||||
}
|
||||
if e, _ := slurpRemainder(c, f); e != nil {
|
||||
t <- &Token{Error: e}
|
||||
|
||||
if e, _ := slurpRemainder(zp.c, zp.file); e != nil {
|
||||
zp.parseErr = e
|
||||
return nil, false
|
||||
}
|
||||
name, ok := toAbsoluteName(l.token, origin)
|
||||
|
||||
name, ok := toAbsoluteName(l.token, zp.origin)
|
||||
if !ok {
|
||||
t <- &Token{Error: &ParseError{f, "bad origin name", l}}
|
||||
return
|
||||
return zp.setParseError("bad origin name", l)
|
||||
}
|
||||
origin = name
|
||||
|
||||
zp.origin = name
|
||||
|
||||
st = zExpectOwnerDir
|
||||
case zExpectDirGenerateBl:
|
||||
if l.value != zBlank {
|
||||
t <- &Token{Error: &ParseError{f, "no blank after $GENERATE-directive", l}}
|
||||
return
|
||||
return zp.setParseError("no blank after $GENERATE-directive", l)
|
||||
}
|
||||
|
||||
st = zExpectDirGenerate
|
||||
case zExpectDirGenerate:
|
||||
if l.value != zString {
|
||||
t <- &Token{Error: &ParseError{f, "expecting $GENERATE value, not this...", l}}
|
||||
return
|
||||
return zp.setParseError("expecting $GENERATE value, not this...", l)
|
||||
}
|
||||
if errMsg := generate(l, c, t, origin); errMsg != "" {
|
||||
t <- &Token{Error: &ParseError{f, errMsg, l}}
|
||||
return
|
||||
}
|
||||
st = zExpectOwnerDir
|
||||
|
||||
return zp.generate(l)
|
||||
case zExpectOwnerBl:
|
||||
if l.value != zBlank {
|
||||
t <- &Token{Error: &ParseError{f, "no blank after owner", l}}
|
||||
return
|
||||
return zp.setParseError("no blank after owner", l)
|
||||
}
|
||||
|
||||
st = zExpectAny
|
||||
case zExpectAny:
|
||||
switch l.value {
|
||||
case zRrtpe:
|
||||
if defttl == nil {
|
||||
t <- &Token{Error: &ParseError{f, "missing TTL with no previous value", l}}
|
||||
return
|
||||
if zp.defttl == nil {
|
||||
return zp.setParseError("missing TTL with no previous value", l)
|
||||
}
|
||||
|
||||
h.Rrtype = l.torc
|
||||
|
||||
st = zExpectRdata
|
||||
case zClass:
|
||||
h.Class = l.torc
|
||||
|
||||
st = zExpectAnyNoClassBl
|
||||
case zString:
|
||||
ttl, ok := stringToTTL(l.token)
|
||||
if !ok {
|
||||
t <- &Token{Error: &ParseError{f, "not a TTL", l}}
|
||||
return
|
||||
return zp.setParseError("not a TTL", l)
|
||||
}
|
||||
|
||||
h.Ttl = ttl
|
||||
if defttl == nil || !defttl.isByDirective {
|
||||
defttl = &ttlState{ttl, false}
|
||||
|
||||
if zp.defttl == nil || !zp.defttl.isByDirective {
|
||||
zp.defttl = &ttlState{ttl, false}
|
||||
}
|
||||
|
||||
st = zExpectAnyNoTTLBl
|
||||
default:
|
||||
t <- &Token{Error: &ParseError{f, "expecting RR type, TTL or class, not this...", l}}
|
||||
return
|
||||
return zp.setParseError("expecting RR type, TTL or class, not this...", l)
|
||||
}
|
||||
case zExpectAnyNoClassBl:
|
||||
if l.value != zBlank {
|
||||
t <- &Token{Error: &ParseError{f, "no blank before class", l}}
|
||||
return
|
||||
return zp.setParseError("no blank before class", l)
|
||||
}
|
||||
|
||||
st = zExpectAnyNoClass
|
||||
case zExpectAnyNoTTLBl:
|
||||
if l.value != zBlank {
|
||||
t <- &Token{Error: &ParseError{f, "no blank before TTL", l}}
|
||||
return
|
||||
return zp.setParseError("no blank before TTL", l)
|
||||
}
|
||||
|
||||
st = zExpectAnyNoTTL
|
||||
case zExpectAnyNoTTL:
|
||||
switch l.value {
|
||||
case zClass:
|
||||
h.Class = l.torc
|
||||
|
||||
st = zExpectRrtypeBl
|
||||
case zRrtpe:
|
||||
h.Rrtype = l.torc
|
||||
|
||||
st = zExpectRdata
|
||||
default:
|
||||
t <- &Token{Error: &ParseError{f, "expecting RR type or class, not this...", l}}
|
||||
return
|
||||
return zp.setParseError("expecting RR type or class, not this...", l)
|
||||
}
|
||||
case zExpectAnyNoClass:
|
||||
switch l.value {
|
||||
case zString:
|
||||
ttl, ok := stringToTTL(l.token)
|
||||
if !ok {
|
||||
t <- &Token{Error: &ParseError{f, "not a TTL", l}}
|
||||
return
|
||||
return zp.setParseError("not a TTL", l)
|
||||
}
|
||||
|
||||
h.Ttl = ttl
|
||||
if defttl == nil || !defttl.isByDirective {
|
||||
defttl = &ttlState{ttl, false}
|
||||
|
||||
if zp.defttl == nil || !zp.defttl.isByDirective {
|
||||
zp.defttl = &ttlState{ttl, false}
|
||||
}
|
||||
|
||||
st = zExpectRrtypeBl
|
||||
case zRrtpe:
|
||||
h.Rrtype = l.torc
|
||||
|
||||
st = zExpectRdata
|
||||
default:
|
||||
t <- &Token{Error: &ParseError{f, "expecting RR type or TTL, not this...", l}}
|
||||
return
|
||||
return zp.setParseError("expecting RR type or TTL, not this...", l)
|
||||
}
|
||||
case zExpectRrtypeBl:
|
||||
if l.value != zBlank {
|
||||
t <- &Token{Error: &ParseError{f, "no blank before RR type", l}}
|
||||
return
|
||||
return zp.setParseError("no blank before RR type", l)
|
||||
}
|
||||
|
||||
st = zExpectRrtype
|
||||
case zExpectRrtype:
|
||||
if l.value != zRrtpe {
|
||||
t <- &Token{Error: &ParseError{f, "unknown RR type", l}}
|
||||
return
|
||||
return zp.setParseError("unknown RR type", l)
|
||||
}
|
||||
|
||||
h.Rrtype = l.torc
|
||||
|
||||
st = zExpectRdata
|
||||
case zExpectRdata:
|
||||
r, e, c1 := setRR(h, c, origin, f)
|
||||
r, e, c1 := setRR(*h, zp.c, zp.origin, zp.file)
|
||||
if e != nil {
|
||||
// If e.lex is nil than we have encounter a unknown RR type
|
||||
// in that case we substitute our current lex token
|
||||
if e.lex.token == "" && e.lex.value == 0 {
|
||||
e.lex = l // Uh, dirty
|
||||
}
|
||||
t <- &Token{Error: e}
|
||||
return
|
||||
|
||||
zp.parseErr = e
|
||||
return nil, false
|
||||
}
|
||||
t <- &Token{RR: r, Comment: c1}
|
||||
st = zExpectOwnerDir
|
||||
|
||||
zp.com = c1
|
||||
return r, true
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, we and the h.Rrtype is still zero, we haven't parsed anything, this
|
||||
// is not an error, because an empty zone file is still a zone file.
|
||||
|
||||
// Surface any read errors from r.
|
||||
if err := c.Err(); err != nil {
|
||||
t <- &Token{Error: &ParseError{file: f, err: err.Error()}}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
type zlexer struct {
|
||||
|
@ -900,6 +1094,11 @@ func (zl *zlexer) Next() (lex, bool) {
|
|||
}
|
||||
}
|
||||
|
||||
if zl.readErr != nil && zl.readErr != io.EOF {
|
||||
// Don't return any tokens after a read error occurs.
|
||||
return lex{value: zEOF}, false
|
||||
}
|
||||
|
||||
var retL lex
|
||||
if stri > 0 {
|
||||
// Send remainder of str
|
||||
|
|
92
scan_test.go
92
scan_test.go
|
@ -39,6 +39,10 @@ func TestParseZoneGenerate(t *testing.T) {
|
|||
}
|
||||
wantIdx++
|
||||
}
|
||||
|
||||
if wantIdx != len(wantRRs) {
|
||||
t.Errorf("too few records, expected %d, got %d", len(wantRRs), wantIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseZoneInclude(t *testing.T) {
|
||||
|
@ -47,6 +51,7 @@ func TestParseZoneInclude(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("could not create tmpfile for test: %s", err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.WriteString("foo\tIN\tA\t127.0.0.1"); err != nil {
|
||||
t.Fatalf("unable to write content to tmpfile %q: %s", tmpfile.Name(), err)
|
||||
|
@ -55,16 +60,24 @@ func TestParseZoneInclude(t *testing.T) {
|
|||
t.Fatalf("could not close tmpfile %q: %s", tmpfile.Name(), err)
|
||||
}
|
||||
|
||||
zone := "$ORIGIN example.org.\n$INCLUDE " + tmpfile.Name()
|
||||
zone := "$ORIGIN example.org.\n$INCLUDE " + tmpfile.Name() + "\nbar\tIN\tA\t127.0.0.2"
|
||||
|
||||
var got int
|
||||
tok := ParseZone(strings.NewReader(zone), "", "")
|
||||
for x := range tok {
|
||||
if x.Error != nil {
|
||||
t.Fatalf("expected no error, but got %s", x.Error)
|
||||
}
|
||||
if x.RR.Header().Name != "foo.example.org." {
|
||||
t.Fatalf("expected %s, but got %s", "foo.example.org.", x.RR.Header().Name)
|
||||
switch x.RR.Header().Name {
|
||||
case "foo.example.org.", "bar.example.org.":
|
||||
default:
|
||||
t.Fatalf("expected foo.example.org. or bar.example.org., but got %s", x.RR.Header().Name)
|
||||
}
|
||||
got++
|
||||
}
|
||||
|
||||
if expected := 2; got != expected {
|
||||
t.Errorf("failed to parse zone after include, expected %d records, got %d", expected, got)
|
||||
}
|
||||
|
||||
os.Remove(tmpfile.Name())
|
||||
|
@ -75,12 +88,39 @@ func TestParseZoneInclude(t *testing.T) {
|
|||
t.Fatalf("expected first token to contain an error but it didn't")
|
||||
}
|
||||
if !strings.Contains(x.Error.Error(), "failed to open") ||
|
||||
!strings.Contains(x.Error.Error(), tmpfile.Name()) {
|
||||
t.Fatalf(`expected error to contain: "failed to open" and %q but got: %s`, tmpfile.Name(), x.Error)
|
||||
!strings.Contains(x.Error.Error(), tmpfile.Name()) ||
|
||||
!strings.Contains(x.Error.Error(), "no such file or directory") {
|
||||
t.Fatalf(`expected error to contain: "failed to open", %q and "no such file or directory" but got: %s`,
|
||||
tmpfile.Name(), x.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestZoneParserIncludeDisallowed(t *testing.T) {
|
||||
tmpfile, err := ioutil.TempFile("", "dns")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create tmpfile for test: %s", err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.WriteString("foo\tIN\tA\t127.0.0.1"); err != nil {
|
||||
t.Fatalf("unable to write content to tmpfile %q: %s", tmpfile.Name(), err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatalf("could not close tmpfile %q: %s", tmpfile.Name(), err)
|
||||
}
|
||||
|
||||
zp := NewZoneParser(strings.NewReader("$INCLUDE "+tmpfile.Name()), "example.org.", "")
|
||||
|
||||
for _, ok := zp.Next(); ok; _, ok = zp.Next() {
|
||||
}
|
||||
|
||||
const expect = "$INCLUDE directive not allowed"
|
||||
if err := zp.Err(); err == nil || !strings.Contains(err.Error(), expect) {
|
||||
t.Errorf("expected error to contain %q, got %v", expect, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTA(t *testing.T) {
|
||||
rr, err := NewRR(` Ta 0 0 0`)
|
||||
if err != nil {
|
||||
|
@ -134,3 +174,45 @@ func BenchmarkReadRR(b *testing.B) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const benchZone = `
|
||||
foo. IN A 10.0.0.1 ; this is comment 1
|
||||
foo. IN A (
|
||||
10.0.0.2 ; this is comment 2
|
||||
)
|
||||
; this is comment 3
|
||||
foo. IN A 10.0.0.3
|
||||
foo. IN A ( 10.0.0.4 ); this is comment 4
|
||||
|
||||
foo. IN A 10.0.0.5
|
||||
; this is comment 5
|
||||
|
||||
foo. IN A 10.0.0.6
|
||||
|
||||
foo. IN DNSKEY 256 3 5 AwEAAb+8l ; this is comment 6
|
||||
foo. IN NSEC miek.nl. TXT RRSIG NSEC; this is comment 7
|
||||
foo. IN TXT "THIS IS TEXT MAN"; this is comment 8
|
||||
`
|
||||
|
||||
func BenchmarkParseZone(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
for tok := range ParseZone(strings.NewReader(benchZone), "example.org.", "") {
|
||||
if tok.Error != nil {
|
||||
b.Fatal(tok.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkZoneParser(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
zp := NewZoneParser(strings.NewReader(benchZone), "example.org.", "")
|
||||
|
||||
for _, ok := zp.Next(); ok; _, ok = zp.Next() {
|
||||
}
|
||||
|
||||
if err := zp.Err(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue