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  }