middleware/hosts for /etc/hosts parsing (#695)

* add hosts middleware

* forgot pointer receiver

* add appropriately modified hostsfile tests from golang repo

* remove test artifacts, separate hostsfile parsing from caching and opening, remove unused metrics references, move middleware up the chain

* refactored the logic for creating records and filtering ip address versions. also got PTR lookups working

* Add README.md. Modify config to be more concise. Add zones list to config. Filter PTR responses based on zones list.

* add Fallthrough and return correct dns response code otherwise

* Simplified Hostsfile to only store hosts in the zones we care about, and by ip version. Added handler tests and improved other tests.

* oops, goimports loaded a package from a different repo
This commit is contained in:
Pat Moroney
2017-06-08 13:48:04 -06:00
committed by Miek Gieben
parent 990460ee7c
commit 92dd947c51
10 changed files with 864 additions and 8 deletions

View File

@@ -26,6 +26,7 @@ var directives = []string{
"loadbalance", "loadbalance",
"dnssec", "dnssec",
"reverse", "reverse",
"hosts",
"kubernetes", "kubernetes",
"file", "file",
"auto", "auto",

View File

@@ -14,6 +14,7 @@ import (
_ "github.com/coredns/coredns/middleware/etcd" _ "github.com/coredns/coredns/middleware/etcd"
_ "github.com/coredns/coredns/middleware/file" _ "github.com/coredns/coredns/middleware/file"
_ "github.com/coredns/coredns/middleware/health" _ "github.com/coredns/coredns/middleware/health"
_ "github.com/coredns/coredns/middleware/hosts"
_ "github.com/coredns/coredns/middleware/kubernetes" _ "github.com/coredns/coredns/middleware/kubernetes"
_ "github.com/coredns/coredns/middleware/loadbalance" _ "github.com/coredns/coredns/middleware/loadbalance"
_ "github.com/coredns/coredns/middleware/log" _ "github.com/coredns/coredns/middleware/log"

View File

@@ -34,13 +34,14 @@
120:loadbalance:loadbalance 120:loadbalance:loadbalance
130:dnssec:dnssec 130:dnssec:dnssec
140:reverse:reverse 140:reverse:reverse
150:kubernetes:kubernetes 150:hosts:hosts
160:file:file 160:kubernetes:kubernetes
170:auto:auto 170:file:file
180:secondary:secondary 180:auto:auto
190:etcd:etcd 190:secondary:secondary
200:proxy:proxy 200:etcd:etcd
210:whoami:whoami 210:proxy:proxy
220:erratic:erratic 220:whoami:whoami
230:erratic:erratic
500:startup:github.com/mholt/caddy/startupshutdown 500:startup:github.com/mholt/caddy/startupshutdown
510:shutdown:github.com/mholt/caddy/startupshutdown 510:shutdown:github.com/mholt/caddy/startupshutdown

View File

@@ -0,0 +1,45 @@
# hosts
*hosts* enables serving zone data from a /etc/hosts style file.
The hosts middleware is useful for serving zones from a /etc/hosts file. It serves from a preloaded
file that exists on disk. It checks the file for changes and updates the zones accordingly. This
middleware only supports A, AAAA, and PTR records. The hosts middleware can be used with readily
available hosts files that block access to advertising servers.
## Syntax
~~~
hosts FILE [ZONES...] {
fallthrough
}
~~~
* **FILE** the hosts file to read and parse. If the path is relative the path from the *root*
directive will be prepended to it. Defaults to /etc/hosts if omitted
* **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block
are used.
* `fallthrough` If zone matches and no record can be generated, pass request to the next middleware.
## Examples
Load /etc/hosts file
~~~
hosts
~~~
Load example.hosts file
~~~
hosts example.hosts
~~~
Load example.hosts file and only serve example.org and example.net from it and fall through to the
next middleware if query doesn't match
~~~
hosts example.hosts example.org example.net {
fallthrough
}
~~~

116
middleware/hosts/hosts.go Normal file
View File

@@ -0,0 +1,116 @@
package hosts
import (
"errors"
"net"
"golang.org/x/net/context"
"github.com/coredns/coredns/middleware"
"github.com/coredns/coredns/middleware/pkg/dnsutil"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// Hosts is the middleware handler
type Hosts struct {
Next middleware.Handler
*Hostsfile
Fallthrough bool
}
// ServeDNS implements the middleware.Handle interface.
func (h Hosts) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
if state.QClass() != dns.ClassINET {
return dns.RcodeServerFailure, middleware.Error(h.Name(), errors.New("can only deal with ClassINET"))
}
qname := state.Name()
answers := []dns.RR{}
zone := middleware.Zones(h.Origins).Matches(qname)
if zone == "" {
// PTR zones don't need to be specified in Origins
if state.Type() != "PTR" {
// If this doesn't match we need to fall through regardless of h.Fallthrough
return middleware.NextOrFailure(h.Name(), h.Next, ctx, w, r)
}
}
switch state.QType() {
case dns.TypePTR:
names := h.LookupStaticAddr(dnsutil.ExtractAddressFromReverse(qname))
if len(names) == 0 {
// If this doesn't match we need to fall through regardless of h.Fallthrough
return middleware.NextOrFailure(h.Name(), h.Next, ctx, w, r)
}
answers = h.ptr(qname, names)
case dns.TypeA:
ips := h.LookupStaticHostV4(qname)
answers = a(qname, ips)
case dns.TypeAAAA:
ips := h.LookupStaticHostV6(qname)
answers = aaaa(qname, ips)
}
if len(answers) == 0 {
if h.Fallthrough {
return middleware.NextOrFailure(h.Name(), h.Next, ctx, w, r)
}
return dns.RcodeRefused, nil
}
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
m.Answer = answers
state.SizeAndDo(m)
m, _ = state.Scrub(m)
w.WriteMsg(m)
return dns.RcodeSuccess, nil
}
// Name implements the middleware.Handle interface.
func (h Hosts) Name() string { return "hosts" }
// a takes a slice of net.IPs and returns a slice of A RRs.
func a(zone string, ips []net.IP) []dns.RR {
answers := []dns.RR{}
for _, ip := range ips {
r := new(dns.A)
r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypeA,
Class: dns.ClassINET, Ttl: 3600}
r.A = ip
answers = append(answers, r)
}
return answers
}
// aaaa takes a slice of net.IPs and returns a slice of AAAA RRs.
func aaaa(zone string, ips []net.IP) []dns.RR {
answers := []dns.RR{}
for _, ip := range ips {
r := new(dns.AAAA)
r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypeAAAA,
Class: dns.ClassINET, Ttl: 3600}
r.AAAA = ip
answers = append(answers, r)
}
return answers
}
// ptr takes a slice of host names and filters out the ones that aren't in Origins, if specified, and returns a slice of PTR RRs.
func (h *Hosts) ptr(zone string, names []string) []dns.RR {
answers := []dns.RR{}
for _, n := range names {
r := new(dns.PTR)
r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypePTR,
Class: dns.ClassINET, Ttl: 3600}
r.Ptr = dns.Fqdn(n)
answers = append(answers, r)
}
return answers
}

View File

@@ -0,0 +1,86 @@
package hosts
import (
"sort"
"strings"
"testing"
"time"
"github.com/coredns/coredns/middleware/pkg/dnsrecorder"
"github.com/coredns/coredns/middleware/test"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
func TestLookupA(t *testing.T) {
h := Hosts{Next: test.ErrorHandler(), Hostsfile: &Hostsfile{expire: time.Now().Add(1 * time.Hour), Origins: []string{"."}}}
h.Parse(strings.NewReader(hostsExample))
ctx := context.TODO()
for _, tc := range hostsTestCases {
m := tc.Msg()
rec := dnsrecorder.New(&test.ResponseWriter{})
_, err := h.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("Expected no error, got %v\n", err)
return
}
resp := rec.Msg
sort.Sort(test.RRSet(resp.Answer))
sort.Sort(test.RRSet(resp.Ns))
sort.Sort(test.RRSet(resp.Extra))
if !test.Header(t, tc, resp) {
t.Logf("%v\n", resp)
continue
}
if !test.Section(t, tc, test.Answer, resp.Answer) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc, test.Ns, resp.Ns) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc, test.Extra, resp.Extra) {
t.Logf("%v\n", resp)
}
}
}
var hostsTestCases = []test.Case{
{
Qname: "example.org.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.A("example.org. 3600 IN A 10.0.0.1"),
},
},
{
Qname: "localhost.", Qtype: dns.TypeAAAA,
Answer: []dns.RR{
test.AAAA("localhost. 3600 IN AAAA ::1"),
},
},
{
Qname: "1.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("1.0.0.10.in-addr.arpa. 3600 PTR example.org."),
},
},
{
Qname: "1.0.0.127.in-addr.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("1.0.0.127.in-addr.arpa. 3600 PTR localhost."),
test.PTR("1.0.0.127.in-addr.arpa. 3600 PTR localhost.domain."),
},
},
}
const hostsExample = `
127.0.0.1 localhost localhost.domain
::1 localhost localhost.domain
10.0.0.1 example.org`

View File

@@ -0,0 +1,193 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file is a modified version of net/hosts.go from the golang repo
package hosts
import (
"bufio"
"bytes"
"io"
"net"
"os"
"strings"
"sync"
"time"
"github.com/coredns/coredns/middleware"
)
const cacheMaxAge = 5 * time.Second
func parseLiteralIP(addr string) net.IP {
if i := strings.Index(addr, "%"); i >= 0 {
// discard ipv6 zone
addr = addr[0:i]
}
return net.ParseIP(addr)
}
func absDomainName(b string) string {
return middleware.Name(b).Normalize()
}
// Hostsfile contains known host entries.
type Hostsfile struct {
sync.Mutex
// list of zones we are authoritive for
Origins []string
// Key for the list of literal IP addresses must be a host
// name. It would be part of DNS labels, a FQDN or an absolute
// FQDN.
// For now the key is converted to lower case for convenience.
byNameV4 map[string][]net.IP
byNameV6 map[string][]net.IP
// Key for the list of host names must be a literal IP address
// including IPv6 address with zone identifier.
// We don't support old-classful IP address notation.
byAddr map[string][]string
expire time.Time
path string
mtime time.Time
size int64
}
// ReadHosts determines if the cached data needs to be updated based on the size and modification time of the hostsfile.
func (h *Hostsfile) ReadHosts() {
now := time.Now()
if now.Before(h.expire) && len(h.byAddr) > 0 {
return
}
stat, err := os.Stat(h.path)
if err == nil && h.mtime.Equal(stat.ModTime()) && h.size == stat.Size() {
h.expire = now.Add(cacheMaxAge)
return
}
var file *os.File
if file, _ = os.Open(h.path); file == nil {
return
}
defer file.Close()
h.Parse(file)
// Update the data cache.
h.expire = now.Add(cacheMaxAge)
h.mtime = stat.ModTime()
h.size = stat.Size()
}
// Parse reads the hostsfile and populates the byName and byAddr maps.
func (h *Hostsfile) Parse(file io.Reader) {
hsv4 := make(map[string][]net.IP)
hsv6 := make(map[string][]net.IP)
is := make(map[string][]string)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Bytes()
if i := bytes.Index(line, []byte{'#'}); i >= 0 {
// Discard comments.
line = line[0:i]
}
f := bytes.Fields(line)
if len(f) < 2 {
continue
}
addr := parseLiteralIP(string(f[0]))
if addr == nil {
continue
}
ver := ipVersion(string(f[0]))
for i := 1; i < len(f); i++ {
name := absDomainName(string(f[i]))
if middleware.Zones(h.Origins).Matches(name) == "" {
// name is not in Origins
continue
}
switch ver {
case 4:
hsv4[name] = append(hsv4[name], addr)
case 6:
hsv6[name] = append(hsv6[name], addr)
default:
continue
}
is[addr.String()] = append(is[addr.String()], name)
}
}
h.byNameV4 = hsv4
h.byNameV6 = hsv6
h.byAddr = is
}
// ipVersion returns what IP version was used textually
func ipVersion(s string) int {
for i := 0; i < len(s); i++ {
switch s[i] {
case '.':
return 4
case ':':
return 6
}
}
return 0
}
// LookupStaticHostV4 looks up the IPv4 addresses for the given host from the hosts file.
func (h *Hostsfile) LookupStaticHostV4(host string) []net.IP {
h.Lock()
defer h.Unlock()
h.ReadHosts()
if len(h.byNameV4) != 0 {
if ips, ok := h.byNameV4[absDomainName(host)]; ok {
ipsCp := make([]net.IP, len(ips))
copy(ipsCp, ips)
return ipsCp
}
}
return nil
}
// LookupStaticHostV6 looks up the IPv6 addresses for the given host from the hosts file.
func (h *Hostsfile) LookupStaticHostV6(host string) []net.IP {
h.Lock()
defer h.Unlock()
h.ReadHosts()
if len(h.byNameV6) != 0 {
if ips, ok := h.byNameV6[absDomainName(host)]; ok {
ipsCp := make([]net.IP, len(ips))
copy(ipsCp, ips)
return ipsCp
}
}
return nil
}
// LookupStaticAddr looks up the hosts for the given address from the hosts file.
func (h *Hostsfile) LookupStaticAddr(addr string) []string {
h.Lock()
defer h.Unlock()
h.ReadHosts()
addr = parseLiteralIP(addr).String()
if addr == "" {
return nil
}
if len(h.byAddr) != 0 {
if hosts, ok := h.byAddr[addr]; ok {
hostsCp := make([]string, len(hosts))
copy(hostsCp, hosts)
return hostsCp
}
}
return nil
}

View File

@@ -0,0 +1,239 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package hosts
import (
"net"
"reflect"
"strings"
"testing"
"time"
)
func testHostsfile(file string) *Hostsfile {
h := &Hostsfile{expire: time.Now().Add(1 * time.Hour), Origins: []string{"."}}
h.Parse(strings.NewReader(file))
return h
}
type staticHostEntry struct {
in string
v4 []string
v6 []string
}
var (
hosts = `255.255.255.255 broadcasthost
127.0.0.2 odin
127.0.0.3 odin # inline comment
::2 odin
127.1.1.1 thor
# aliases
127.1.1.2 ullr ullrhost
fe80::1%lo0 localhost
# Bogus entries that must be ignored.
123.123.123 loki
321.321.321.321`
singlelinehosts = `127.0.0.2 odin`
ipv4hosts = `# See https://tools.ietf.org/html/rfc1123.
#
# The literal IPv4 address parser in the net package is a relaxed
# one. It may accept a literal IPv4 address in dotted-decimal notation
# with leading zeros such as "001.2.003.4".
# internet address and host name
127.0.0.1 localhost # inline comment separated by tab
127.000.000.002 localhost # inline comment separated by space
# internet address, host name and aliases
127.000.000.003 localhost localhost.localdomain`
ipv6hosts = `# See https://tools.ietf.org/html/rfc5952, https://tools.ietf.org/html/rfc4007.
# internet address and host name
::1 localhost # inline comment separated by tab
fe80:0000:0000:0000:0000:0000:0000:0001 localhost # inline comment separated by space
# internet address with zone identifier and host name
fe80:0000:0000:0000:0000:0000:0000:0002%lo0 localhost
# internet address, host name and aliases
fe80::3%lo0 localhost localhost.localdomain`
casehosts = `127.0.0.1 PreserveMe PreserveMe.local
::1 PreserveMe PreserveMe.local`
)
var lookupStaticHostTests = []struct {
file string
ents []staticHostEntry
}{
{
hosts,
[]staticHostEntry{
{"odin", []string{"127.0.0.2", "127.0.0.3"}, []string{"::2"}},
{"thor", []string{"127.1.1.1"}, []string{}},
{"ullr", []string{"127.1.1.2"}, []string{}},
{"ullrhost", []string{"127.1.1.2"}, []string{}},
{"localhost", []string{}, []string{"fe80::1"}},
},
},
{
singlelinehosts, // see golang.org/issue/6646
[]staticHostEntry{
{"odin", []string{"127.0.0.2"}, []string{}},
},
},
{
ipv4hosts,
[]staticHostEntry{
{"localhost", []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, []string{}},
{"localhost.localdomain", []string{"127.0.0.3"}, []string{}},
},
},
{
ipv6hosts,
[]staticHostEntry{
{"localhost", []string{}, []string{"::1", "fe80::1", "fe80::2", "fe80::3"}},
{"localhost.localdomain", []string{}, []string{"fe80::3"}},
},
},
{
casehosts,
[]staticHostEntry{
{"PreserveMe", []string{"127.0.0.1"}, []string{"::1"}},
{"PreserveMe.local", []string{"127.0.0.1"}, []string{"::1"}},
},
},
}
func TestLookupStaticHost(t *testing.T) {
for _, tt := range lookupStaticHostTests {
h := testHostsfile(tt.file)
for _, ent := range tt.ents {
testStaticHost(t, ent, h)
}
}
}
func testStaticHost(t *testing.T, ent staticHostEntry, h *Hostsfile) {
ins := []string{ent.in, absDomainName(ent.in), strings.ToLower(ent.in), strings.ToUpper(ent.in)}
for k, in := range ins {
addrsV4 := h.LookupStaticHostV4(in)
if len(addrsV4) != len(ent.v4) {
t.Fatalf("%d, lookupStaticHostV4(%s) = %v; want %v", k, in, addrsV4, ent.v4)
}
for i, v4 := range addrsV4 {
if v4.String() != ent.v4[i] {
t.Fatalf("%d, lookupStaticHostV4(%s) = %v; want %v", k, in, addrsV4, ent.v4)
}
}
addrsV6 := h.LookupStaticHostV6(in)
if len(addrsV6) != len(ent.v6) {
t.Fatalf("%d, lookupStaticHostV6(%s) = %v; want %v", k, in, addrsV6, ent.v6)
}
for i, v6 := range addrsV6 {
if v6.String() != ent.v6[i] {
t.Fatalf("%d, lookupStaticHostV6(%s) = %v; want %v", k, in, addrsV6, ent.v6)
}
}
}
}
type staticIPEntry struct {
in string
out []string
}
var lookupStaticAddrTests = []struct {
file string
ents []staticIPEntry
}{
{
hosts,
[]staticIPEntry{
{"255.255.255.255", []string{"broadcasthost"}},
{"127.0.0.2", []string{"odin"}},
{"127.0.0.3", []string{"odin"}},
{"::2", []string{"odin"}},
{"127.1.1.1", []string{"thor"}},
{"127.1.1.2", []string{"ullr", "ullrhost"}},
{"fe80::1", []string{"localhost"}},
},
},
{
singlelinehosts, // see golang.org/issue/6646
[]staticIPEntry{
{"127.0.0.2", []string{"odin"}},
},
},
{
ipv4hosts, // see golang.org/issue/8996
[]staticIPEntry{
{"127.0.0.1", []string{"localhost"}},
{"127.0.0.2", []string{"localhost"}},
{"127.0.0.3", []string{"localhost", "localhost.localdomain"}},
},
},
{
ipv6hosts, // see golang.org/issue/8996
[]staticIPEntry{
{"::1", []string{"localhost"}},
{"fe80::1", []string{"localhost"}},
{"fe80::2", []string{"localhost"}},
{"fe80::3", []string{"localhost", "localhost.localdomain"}},
},
},
{
casehosts, // see golang.org/issue/12806
[]staticIPEntry{
{"127.0.0.1", []string{"PreserveMe", "PreserveMe.local"}},
{"::1", []string{"PreserveMe", "PreserveMe.local"}},
},
},
}
func TestLookupStaticAddr(t *testing.T) {
for _, tt := range lookupStaticAddrTests {
h := testHostsfile(tt.file)
for _, ent := range tt.ents {
testStaticAddr(t, ent, h)
}
}
}
func testStaticAddr(t *testing.T, ent staticIPEntry, h *Hostsfile) {
hosts := h.LookupStaticAddr(ent.in)
for i := range ent.out {
ent.out[i] = absDomainName(ent.out[i])
}
if !reflect.DeepEqual(hosts, ent.out) {
t.Errorf("%s, lookupStaticAddr(%s) = %v; want %v", h.path, ent.in, hosts, h)
}
}
func TestHostCacheModification(t *testing.T) {
// Ensure that programs can't modify the internals of the host cache.
// See https://github.com/golang/go/issues/14212.
h := testHostsfile(ipv4hosts)
ent := staticHostEntry{"localhost", []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, []string{}}
testStaticHost(t, ent, h)
// Modify the addresses return by lookupStaticHost.
addrs := h.LookupStaticHostV6(ent.in)
for i := range addrs {
addrs[i] = net.IPv4zero
}
testStaticHost(t, ent, h)
h = testHostsfile(ipv6hosts)
entip := staticIPEntry{"::1", []string{"localhost"}}
testStaticAddr(t, entip, h)
// Modify the hosts return by lookupStaticAddr.
hosts := h.LookupStaticAddr(entip.in)
for i := range hosts {
hosts[i] += "junk"
}
testStaticAddr(t, entip, h)
}

88
middleware/hosts/setup.go Normal file
View File

@@ -0,0 +1,88 @@
package hosts
import (
"log"
"os"
"path"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/middleware"
"github.com/mholt/caddy"
)
func init() {
caddy.RegisterPlugin("hosts", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
func setup(c *caddy.Controller) error {
h, err := hostsParse(c)
if err != nil {
return middleware.Error("hosts", err)
}
dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler {
h.Next = next
return h
})
return nil
}
func hostsParse(c *caddy.Controller) (Hosts, error) {
var h = Hosts{
Hostsfile: &Hostsfile{path: "/etc/hosts"},
}
defer h.ReadHosts()
config := dnsserver.GetConfig(c)
for c.Next() {
if c.Val() == "hosts" { // hosts [FILE] [ZONES...]
args := c.RemainingArgs()
if len(args) >= 1 {
h.path = args[0]
args = args[1:]
if !path.IsAbs(h.path) && config.Root != "" {
h.path = path.Join(config.Root, h.path)
}
_, err := os.Stat(h.path)
if err != nil {
if os.IsNotExist(err) {
log.Printf("[WARNING] File does not exist: %s", h.path)
} else {
return h, c.Errf("unable to access hosts file '%s': %v", h.path, err)
}
}
}
origins := make([]string, len(c.ServerBlockKeys))
copy(origins, c.ServerBlockKeys)
if len(args) > 0 {
origins = args
}
for i := range origins {
origins[i] = middleware.Host(origins[i]).Normalize()
}
h.Origins = origins
for c.NextBlock() {
switch c.Val() {
case "fallthrough":
args := c.RemainingArgs()
if len(args) == 0 {
h.Fallthrough = true
continue
}
return h, c.ArgErr()
}
}
}
}
return h, nil
}

View File

@@ -0,0 +1,86 @@
package hosts
import (
"testing"
"github.com/mholt/caddy"
)
func TestHostsParse(t *testing.T) {
tests := []struct {
inputFileRules string
shouldErr bool
expectedPath string
expectedOrigins []string
expectedFallthrough bool
}{
{
`hosts
`,
false, "/etc/hosts", nil, false,
},
{
`hosts /tmp`,
false, "/tmp", nil, false,
},
{
`hosts /etc/hosts miek.nl.`,
false, "/etc/hosts", []string{"miek.nl."}, false,
},
{
`hosts /etc/hosts miek.nl. pun.gent.`,
false, "/etc/hosts", []string{"miek.nl.", "pun.gent."}, false,
},
{
`hosts {
fallthrough
}`,
false, "/etc/hosts", nil, true,
},
{
`hosts /tmp {
fallthrough
}`,
false, "/tmp", nil, true,
},
{
`hosts /etc/hosts miek.nl. {
fallthrough
}`,
false, "/etc/hosts", []string{"miek.nl."}, true,
},
{
`hosts /etc/hosts miek.nl. pun.gent. {
fallthrough
}`,
false, "/etc/hosts", []string{"miek.nl.", "pun.gent."}, true,
},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.inputFileRules)
h, err := hostsParse(c)
if err == nil && test.shouldErr {
t.Fatalf("Test %d expected errors, but got no error", i)
} else if err != nil && !test.shouldErr {
t.Fatalf("Test %d expected no errors, but got '%v'", i, err)
} else if !test.shouldErr {
if h.path != test.expectedPath {
t.Fatalf("Test %d expected %v, got %v", i, test.expectedPath, h.path)
}
} else {
if h.Fallthrough != test.expectedFallthrough {
t.Fatalf("Test %d expected fallthrough of %v, got %v", i, test.expectedFallthrough, h.Fallthrough)
}
if len(h.Origins) != len(test.expectedOrigins) {
t.Fatalf("Test %d expected %v, got %v", i, test.expectedOrigins, h.Origins)
}
for j, name := range test.expectedOrigins {
if h.Origins[j] != name {
t.Fatalf("Test %d expected %v for %d th zone, got %v", i, name, j, h.Origins[j])
}
}
}
}
}