master
go 165 lines 3.85 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package config
4
5 import (
6 "bufio"
7 "errors"
8 "fmt"
9 "os"
10 "path/filepath"
11 "runtime"
12 "strings"
13 )
14
15 type option struct{ name, value string }
16
17 const (
18 optInclude = "include"
19 optIncludeToplevel = "include-toplevel"
20 optCumulative = "statistics-cumulative"
21 optEnable = "control-enable"
22 optInterface = "control-interface"
23 optPort = "control-port"
24 optUseCert = "control-use-cert"
25 optKeyFile = "control-key-file"
26 optCertFile = "control-cert-file"
27 )
28
29 func isOptionUsed(opt option) bool {
30 switch opt.name {
31 case optInclude,
32 optIncludeToplevel,
33 optCumulative,
34 optEnable,
35 optInterface,
36 optPort,
37 optUseCert,
38 optKeyFile,
39 optCertFile:
40 return true
41 }
42 return false
43 }
44
45 // TODO:
46 // If also using chroot, using full path names for the included files works, relative pathnames for the included names
47 // work if the directory where the daemon is started equals its chroot/working directory or is specified before
48 // the include statement with directory: dir.
49
50 // Parse parses Unbound configuration files into UnboundConfig.
51 // It follows logic described in the 'man unbound.conf':
52 // - Files can be included using the 'include:' directive. It can appear anywhere, it accepts a single file name as argument.
53 // - Processing continues as if the text from the included file was copied into the config file at that point.
54 // - Wildcards can be used to include multiple files.
55 //
56 // It stops processing on any error: syntax error, recursive include, glob matches directory etc.
57 func Parse(entryPath string) (*UnboundConfig, error) {
58 options, err := parse(entryPath, nil)
59 if err != nil {
60 return nil, err
61 }
62 return fromOptions(options), nil
63 }
64
65 func parse(filename string, visited map[string]bool) ([]option, error) {
66 if visited == nil {
67 visited = make(map[string]bool)
68 }
69 if visited[filename] {
70 return nil, fmt.Errorf("'%s' already visited", filename)
71 }
72 visited[filename] = true
73
74 f, err := open(filename)
75 if err != nil {
76 return nil, err
77 }
78 defer func() { _ = f.Close() }()
79
80 var options []option
81 sc := bufio.NewScanner(f)
82
83 for sc.Scan() {
84 line := strings.TrimSpace(sc.Text())
85 if line == "" || strings.HasPrefix(line, "#") {
86 continue
87 }
88
89 opt, err := parseLine(line)
90 if err != nil {
91 return nil, fmt.Errorf("file '%s', error on parsing line '%s': %v", filename, line, err)
92 }
93
94 if !isOptionUsed(opt) {
95 continue
96 }
97
98 if opt.name != optInclude && opt.name != optIncludeToplevel {
99 options = append(options, opt)
100 continue
101 }
102
103 filenames, err := globInclude(opt.value)
104 if err != nil {
105 return nil, err
106 }
107
108 for _, name := range filenames {
109 opts, err := parse(name, visited)
110 if err != nil {
111 return nil, err
112 }
113 options = append(options, opts...)
114 }
115 }
116 return options, nil
117 }
118
119 func globInclude(include string) ([]string, error) {
120 if isGlobPattern(include) {
121 return filepath.Glob(include)
122 }
123 return []string{include}, nil
124 }
125
126 func parseLine(line string) (option, error) {
127 parts := strings.Split(line, ":")
128 if len(parts) < 2 {
129 return option{}, errors.New("bad syntax")
130 }
131 key, value := cleanKeyValue(parts[0], parts[1])
132 return option{name: key, value: value}, nil
133 }
134
135 func cleanKeyValue(key, value string) (string, string) {
136 if i := strings.IndexByte(value, '#'); i > 0 {
137 value = value[:i-1]
138 }
139 key = strings.TrimSpace(key)
140 value = strings.Trim(strings.TrimSpace(value), "\"'")
141 return key, value
142 }
143
144 func isGlobPattern(value string) bool {
145 magicChars := `*?[`
146 if runtime.GOOS != "windows" {
147 magicChars = `*?[\`
148 }
149 return strings.ContainsAny(value, magicChars)
150 }
151
152 func open(filename string) (*os.File, error) {
153 f, err := os.Open(filename)
154 if err != nil {
155 return nil, err
156 }
157 fi, err := f.Stat()
158 if err != nil {
159 return nil, err
160 }
161 if !fi.Mode().IsRegular() {
162 return nil, fmt.Errorf("'%s' is not a regular file", filename)
163 }
164 return f, nil
165 }