1
0
Fork 0
mirror of https://github.com/gocsaf/csaf.git synced 2025-12-22 11:55:40 +01:00

Add support for config files in downloader. (#404)

* Add support for config files in downloader.

* Add no-ini for the version flag, too.

* Add config file options in doc to downloader.
This commit is contained in:
Sascha L. Teichmann 2023-07-19 10:49:17 +02:00 committed by GitHub
parent de27a668d1
commit 8630e8bac2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 135 additions and 49 deletions

View file

@ -0,0 +1,66 @@
// This file is Free Software under the MIT License
// without warranty, see README.md and LICENSES/MIT.txt for details.
//
// SPDX-License-Identifier: MIT
//
// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2022 Intevation GmbH <https://intevation.de>
package main
import (
"log"
"net/http"
"os"
"github.com/mitchellh/go-homedir"
)
const defaultWorker = 2
type config struct {
Directory *string `short:"d" long:"directory" description:"DIRectory to store the downloaded files in" value-name:"DIR"`
Insecure bool `long:"insecure" description:"Do not check TLS certificates from provider"`
IgnoreSignatureCheck bool `long:"ignoresigcheck" description:"Ignore signature check results, just warn on mismatch"`
Version bool `long:"version" description:"Display version of the binary" no-ini:"true"`
Verbose bool `long:"verbose" short:"v" description:"Verbose output"`
Rate *float64 `long:"rate" short:"r" description:"The average upper limit of https operations per second (defaults to unlimited)"`
Worker int `long:"worker" short:"w" description:"NUMber of concurrent downloads" value-name:"NUM"`
ExtraHeader http.Header `long:"header" short:"H" description:"One or more extra HTTP header fields"`
RemoteValidator string `long:"validator" description:"URL to validate documents remotely" value-name:"URL"`
RemoteValidatorCache string `long:"validatorcache" description:"FILE to cache remote validations" value-name:"FILE"`
RemoteValidatorPresets []string `long:"validatorpreset" description:"One or more presets to validate remotely" default:"mandatory"`
Config *string `short:"c" long:"config" description:"Path to config ini file" value-name:"INI-FILE" no-ini:"true"`
}
// iniPaths are the potential file locations of the the config file.
var iniPaths = []string{
"~/.config/csaf/downloader.ini",
"~/.csaf_downloader.ini",
"csaf_downloader.ini",
}
// findIniFile looks for a file in the pre-defined paths in "iniPaths".
// The returned value will be the name of file if found, otherwise an empty string.
func findIniFile() string {
for _, f := range iniPaths {
name, err := homedir.Expand(f)
if err != nil {
log.Printf("warn: %v\n", err)
continue
}
if _, err := os.Stat(name); err == nil {
return name
}
}
return ""
}
// prepare prepares internal state of a loaded configuration.
func (cfg *config) prepare() error {
// TODO: Implement me!
return nil
}

View file

@ -37,7 +37,7 @@ import (
) )
type downloader struct { type downloader struct {
opts *options cfg *config
directory string directory string
keys *crypto.KeyRing keys *crypto.KeyRing
eval *util.PathEval eval *util.PathEval
@ -45,15 +45,15 @@ type downloader struct {
mkdirMu sync.Mutex mkdirMu sync.Mutex
} }
func newDownloader(opts *options) (*downloader, error) { func newDownloader(cfg *config) (*downloader, error) {
var validator csaf.RemoteValidator var validator csaf.RemoteValidator
if opts.RemoteValidator != "" { if cfg.RemoteValidator != "" {
validatorOptions := csaf.RemoteValidatorOptions{ validatorOptions := csaf.RemoteValidatorOptions{
URL: opts.RemoteValidator, URL: cfg.RemoteValidator,
Presets: opts.RemoteValidatorPresets, Presets: cfg.RemoteValidatorPresets,
Cache: opts.RemoteValidatorCache, Cache: cfg.RemoteValidatorCache,
} }
var err error var err error
if validator, err = validatorOptions.Open(); err != nil { if validator, err = validatorOptions.Open(); err != nil {
@ -64,7 +64,7 @@ func newDownloader(opts *options) (*downloader, error) {
} }
return &downloader{ return &downloader{
opts: opts, cfg: cfg,
eval: util.NewPathEval(), eval: util.NewPathEval(),
validator: validator, validator: validator,
}, nil }, nil
@ -82,7 +82,7 @@ func (d *downloader) httpClient() util.Client {
hClient := http.Client{} hClient := http.Client{}
var tlsConfig tls.Config var tlsConfig tls.Config
if d.opts.Insecure { if d.cfg.Insecure {
tlsConfig.InsecureSkipVerify = true tlsConfig.InsecureSkipVerify = true
hClient.Transport = &http.Transport{ hClient.Transport = &http.Transport{
TLSClientConfig: &tlsConfig, TLSClientConfig: &tlsConfig,
@ -92,23 +92,23 @@ func (d *downloader) httpClient() util.Client {
client := util.Client(&hClient) client := util.Client(&hClient)
// Add extra headers. // Add extra headers.
if len(d.opts.ExtraHeader) > 0 { if len(d.cfg.ExtraHeader) > 0 {
client = &util.HeaderClient{ client = &util.HeaderClient{
Client: client, Client: client,
Header: d.opts.ExtraHeader, Header: d.cfg.ExtraHeader,
} }
} }
// Add optional URL logging. // Add optional URL logging.
if d.opts.Verbose { if d.cfg.Verbose {
client = &util.LoggingClient{Client: client} client = &util.LoggingClient{Client: client}
} }
// Add optional rate limiting. // Add optional rate limiting.
if d.opts.Rate != nil { if d.cfg.Rate != nil {
client = &util.LimitingClient{ client = &util.LimitingClient{
Client: client, Client: client,
Limiter: rate.NewLimiter(rate.Limit(*d.opts.Rate), 1), Limiter: rate.NewLimiter(rate.Limit(*d.cfg.Rate), 1),
} }
} }
@ -122,7 +122,7 @@ func (d *downloader) download(ctx context.Context, domain string) error {
lpmd := loader.Load(domain) lpmd := loader.Load(domain)
if d.opts.Verbose { if d.cfg.Verbose {
for i := range lpmd.Messages { for i := range lpmd.Messages {
log.Printf("Loading provider-metadata.json for %q: %s\n", log.Printf("Loading provider-metadata.json for %q: %s\n",
domain, lpmd.Messages[i].Message) domain, lpmd.Messages[i].Message)
@ -181,7 +181,7 @@ func (d *downloader) downloadFiles(
}() }()
var n int var n int
if n = d.opts.Worker; n < 1 { if n = d.cfg.Worker; n < 1 {
n = 1 n = 1
} }
@ -289,7 +289,7 @@ func (d *downloader) logValidationIssues(url string, errors []string, err error)
return return
} }
if len(errors) > 0 { if len(errors) > 0 {
if d.opts.Verbose { if d.cfg.Verbose {
log.Printf("CSAF file %s has validation errors: %s\n", log.Printf("CSAF file %s has validation errors: %s\n",
url, strings.Join(errors, ", ")) url, strings.Join(errors, ", "))
} else { } else {
@ -372,7 +372,7 @@ nextAdvisory:
// Only hash when we have a remote counter part we can compare it with. // Only hash when we have a remote counter part we can compare it with.
if remoteSHA256, s256Data, err = loadHash(client, file.SHA256URL()); err != nil { if remoteSHA256, s256Data, err = loadHash(client, file.SHA256URL()); err != nil {
if d.opts.Verbose { if d.cfg.Verbose {
log.Printf("WARN: cannot fetch %s: %v\n", file.SHA256URL(), err) log.Printf("WARN: cannot fetch %s: %v\n", file.SHA256URL(), err)
} }
} else { } else {
@ -381,7 +381,7 @@ nextAdvisory:
} }
if remoteSHA512, s512Data, err = loadHash(client, file.SHA512URL()); err != nil { if remoteSHA512, s512Data, err = loadHash(client, file.SHA512URL()); err != nil {
if d.opts.Verbose { if d.cfg.Verbose {
log.Printf("WARN: cannot fetch %s: %v\n", file.SHA512URL(), err) log.Printf("WARN: cannot fetch %s: %v\n", file.SHA512URL(), err)
} }
} else { } else {
@ -423,7 +423,7 @@ nextAdvisory:
var sign *crypto.PGPSignature var sign *crypto.PGPSignature
sign, signData, err = loadSignature(client, file.SignURL()) sign, signData, err = loadSignature(client, file.SignURL())
if err != nil { if err != nil {
if d.opts.Verbose { if d.cfg.Verbose {
log.Printf("downloading signature '%s' failed: %v\n", log.Printf("downloading signature '%s' failed: %v\n",
file.SignURL(), err) file.SignURL(), err)
} }
@ -431,7 +431,7 @@ nextAdvisory:
if sign != nil { if sign != nil {
if err := d.checkSignature(data.Bytes(), sign); err != nil { if err := d.checkSignature(data.Bytes(), sign); err != nil {
log.Printf("Cannot verify signature for %s: %v\n", file.URL(), err) log.Printf("Cannot verify signature for %s: %v\n", file.URL(), err)
if !d.opts.IgnoreSignatureCheck { if !d.cfg.IgnoreSignatureCheck {
continue continue
} }
} }
@ -560,7 +560,7 @@ func loadHash(client util.Client, p string) ([]byte, []byte, error) {
// exists and is setup properly. // exists and is setup properly.
func (d *downloader) prepareDirectory() error { func (d *downloader) prepareDirectory() error {
// If no special given use current working directory. // If no special given use current working directory.
if d.opts.Directory == nil { if d.cfg.Directory == nil {
dir, err := os.Getwd() dir, err := os.Getwd()
if err != nil { if err != nil {
return err return err
@ -569,17 +569,17 @@ func (d *downloader) prepareDirectory() error {
return nil return nil
} }
// Use given directory // Use given directory
if _, err := os.Stat(*d.opts.Directory); err != nil { if _, err := os.Stat(*d.cfg.Directory); err != nil {
// If it does not exist create it. // If it does not exist create it.
if os.IsNotExist(err) { if os.IsNotExist(err) {
if err = os.MkdirAll(*d.opts.Directory, 0755); err != nil { if err = os.MkdirAll(*d.cfg.Directory, 0755); err != nil {
return err return err
} }
} else { } else {
return err return err
} }
} }
d.directory = *d.opts.Directory d.directory = *d.cfg.Directory
return nil return nil
} }

View file

@ -13,32 +13,14 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"net/http"
"os" "os"
"os/signal" "os/signal"
"github.com/csaf-poc/csaf_distribution/v2/util" "github.com/csaf-poc/csaf_distribution/v2/util"
"github.com/jessevdk/go-flags" "github.com/jessevdk/go-flags"
"github.com/mitchellh/go-homedir"
) )
const defaultWorker = 2
type options struct {
Directory *string `short:"d" long:"directory" description:"DIRectory to store the downloaded files in" value-name:"DIR"`
Insecure bool `long:"insecure" description:"Do not check TLS certificates from provider"`
IgnoreSignatureCheck bool `long:"ignoresigcheck" description:"Ignore signature check results, just warn on mismatch"`
Version bool `long:"version" description:"Display version of the binary"`
Verbose bool `long:"verbose" short:"v" description:"Verbose output"`
Rate *float64 `long:"rate" short:"r" description:"The average upper limit of https operations per second (defaults to unlimited)"`
Worker int `long:"worker" short:"w" description:"NUMber of concurrent downloads" value-name:"NUM"`
ExtraHeader http.Header `long:"header" short:"H" description:"One or more extra HTTP header fields"`
RemoteValidator string `long:"validator" description:"URL to validate documents remotely" value-name:"URL"`
RemoteValidatorCache string `long:"validatorcache" description:"FILE to cache remote validations" value-name:"FILE"`
RemoteValidatorPresets []string `long:"validatorpreset" description:"One or more presets to validate remotely" default:"mandatory"`
}
func errCheck(err error) { func errCheck(err error) {
if err != nil { if err != nil {
if flags.WroteHelp(err) { if flags.WroteHelp(err) {
@ -48,8 +30,8 @@ func errCheck(err error) {
} }
} }
func run(opts *options, domains []string) error { func run(cfg *config, domains []string) error {
d, err := newDownloader(opts) d, err := newDownloader(cfg)
if err != nil { if err != nil {
return err return err
} }
@ -65,24 +47,38 @@ func run(opts *options, domains []string) error {
func main() { func main() {
opts := &options{ cfg := &config{
Worker: defaultWorker, Worker: defaultWorker,
} }
parser := flags.NewParser(opts, flags.Default) parser := flags.NewParser(cfg, flags.Default)
parser.Usage = "[OPTIONS] domain..." parser.Usage = "[OPTIONS] domain..."
domains, err := parser.Parse() domains, err := parser.Parse()
errCheck(err) errCheck(err)
if opts.Version { if cfg.Version {
fmt.Println(util.SemVersion) fmt.Println(util.SemVersion)
return return
} }
if cfg.Config != nil {
iniParser := flags.NewIniParser(parser)
iniParser.ParseAsDefaults = true
name, err := homedir.Expand(*cfg.Config)
errCheck(err)
errCheck(iniParser.ParseFile(name))
} else if iniFile := findIniFile(); iniFile != "" {
iniParser := flags.NewIniParser(parser)
iniParser.ParseAsDefaults = true
errCheck(iniParser.ParseFile(iniFile))
}
errCheck(cfg.prepare())
if len(domains) == 0 { if len(domains) == 0 {
log.Println("No domains given.") log.Println("No domains given.")
return return
} }
errCheck(run(opts, domains)) errCheck(run(cfg, domains))
} }

View file

@ -18,6 +18,7 @@ Application Options:
--validator=URL URL to validate documents remotely --validator=URL URL to validate documents remotely
--validatorcache=FILE FILE to cache remote validations --validatorcache=FILE FILE to cache remote validations
--validatorpreset= One or more presets to validate remotely (default: mandatory) --validatorpreset= One or more presets to validate remotely (default: mandatory)
-c, --config=INI-FILE Path to config ini file
Help Options: Help Options:
-h, --help Show this help message -h, --help Show this help message
@ -31,3 +32,26 @@ Increasing the number of workers opens more connections to the web servers
to download more advisories at once. This may improve the overall speed of the download. to download more advisories at once. This may improve the overall speed of the download.
However, since this also increases the load on the servers, their administrators could However, since this also increases the load on the servers, their administrators could
have taken countermeasures to limit this. have taken countermeasures to limit this.
If no config file is explictly given the follwing places are searched for a config file:
```
~/.config/csaf/downloader.ini
~/.csaf_downloader.ini
csaf_downloader.ini
```
with `~` expanding to `$HOME` on unixoid systems and `%HOMEPATH` on Windows systems.
Supported options in config files:
```
directory # not set by default
insecure = false
ignoresigcheck = false
verbose = false
# rate # set to unlimited
worker = 2
# header # not set by default
# validator # not set by default
# validatorcache # not set by default
validatorpreset = "mandatory"
```