github.com/netdata/go.d.plugin@v0.58.1/modules/unbound/config/parse.go (about) 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 }