mirror of
https://github.com/gocsaf/csaf.git
synced 2025-12-22 11:55:40 +01:00
Merge pull request #530 from oxisto/slog
Added support for structured logging in `csaf_aggregator`
This commit is contained in:
commit
617deb4c17
23 changed files with 135 additions and 91 deletions
|
|
@ -69,7 +69,7 @@ Download the binaries from the most recent release assets on Github.
|
||||||
|
|
||||||
### Build from sources
|
### Build from sources
|
||||||
|
|
||||||
- A recent version of **Go** (1.20+) should be installed. [Go installation](https://go.dev/doc/install)
|
- A recent version of **Go** (1.21+) should be installed. [Go installation](https://go.dev/doc/install)
|
||||||
|
|
||||||
- Clone the repository `git clone https://github.com/csaf-poc/csaf_distribution.git `
|
- Clone the repository `git clone https://github.com/csaf-poc/csaf_distribution.git `
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
@ -178,9 +178,11 @@ func (p *provider) ageAccept(c *config) func(time.Time) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Verbose {
|
if c.Verbose {
|
||||||
log.Printf(
|
slog.Debug(
|
||||||
"Setting up filter to accept advisories within time range %s to %s\n",
|
"Setting up filter to accept advisories within time range",
|
||||||
r[0].Format(time.RFC3339), r[1].Format(time.RFC3339))
|
"from", r[0].Format(time.RFC3339),
|
||||||
|
"to", r[1].Format(time.RFC3339),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return r.Contains
|
return r.Contains
|
||||||
}
|
}
|
||||||
|
|
@ -393,6 +395,17 @@ func (c *config) setDefaults() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareLogging sets up the structured logging.
|
||||||
|
func (c *config) prepareLogging() error {
|
||||||
|
ho := slog.HandlerOptions{
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
}
|
||||||
|
handler := slog.NewTextHandler(os.Stdout, &ho)
|
||||||
|
logger := slog.New(handler)
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// compileIgnorePatterns compiles the configured patterns to be ignored.
|
// compileIgnorePatterns compiles the configured patterns to be ignored.
|
||||||
func (p *provider) compileIgnorePatterns() error {
|
func (p *provider) compileIgnorePatterns() error {
|
||||||
pm, err := filter.NewPatternMatcher(p.IgnorePattern)
|
pm, err := filter.NewPatternMatcher(p.IgnorePattern)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -29,11 +29,13 @@ type fullJob struct {
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupProviderFull fetches the provider-metadate.json for a specific provider.
|
// setupProviderFull fetches the provider-metadata.json for a specific provider.
|
||||||
func (w *worker) setupProviderFull(provider *provider) error {
|
func (w *worker) setupProviderFull(provider *provider) error {
|
||||||
log.Printf("worker #%d: %s (%s)\n",
|
w.log.Info("Setting up provider",
|
||||||
w.num, provider.Name, provider.Domain)
|
"provider", slog.GroupValue(
|
||||||
|
slog.String("name", provider.Name),
|
||||||
|
slog.String("domain", provider.Domain),
|
||||||
|
))
|
||||||
w.dir = ""
|
w.dir = ""
|
||||||
w.provider = provider
|
w.provider = provider
|
||||||
|
|
||||||
|
|
@ -55,7 +57,7 @@ func (w *worker) setupProviderFull(provider *provider) error {
|
||||||
"provider-metadata.json has %d validation issues", len(errors))
|
"provider-metadata.json has %d validation issues", len(errors))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("provider-metadata: %s\n", w.loc)
|
w.log.Info("Using provider-metadata", "url", w.loc)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +81,7 @@ func (w *worker) fullWork(wg *sync.WaitGroup, jobs <-chan *fullJob) {
|
||||||
func (p *processor) full() error {
|
func (p *processor) full() error {
|
||||||
|
|
||||||
if p.cfg.runAsMirror() {
|
if p.cfg.runAsMirror() {
|
||||||
log.Println("Running in aggregator mode")
|
p.log.Info("Running in aggregator mode")
|
||||||
|
|
||||||
// check if we need to setup a remote validator
|
// check if we need to setup a remote validator
|
||||||
if p.cfg.RemoteValidatorOptions != nil {
|
if p.cfg.RemoteValidatorOptions != nil {
|
||||||
|
|
@ -96,16 +98,18 @@ func (p *processor) full() error {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Println("Running in lister mode")
|
p.log.Info("Running in lister mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
queue := make(chan *fullJob)
|
queue := make(chan *fullJob)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
log.Printf("Starting %d workers.\n", p.cfg.Workers)
|
p.log.Info("Starting workers...", "num", p.cfg.Workers)
|
||||||
|
|
||||||
for i := 1; i <= p.cfg.Workers; i++ {
|
for i := 1; i <= p.cfg.Workers; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
w := newWorker(i, p)
|
w := newWorker(i, p)
|
||||||
|
|
||||||
go w.fullWork(&wg, queue)
|
go w.fullWork(&wg, queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,12 +139,22 @@ func (p *processor) full() error {
|
||||||
for i := range jobs {
|
for i := range jobs {
|
||||||
j := &jobs[i]
|
j := &jobs[i]
|
||||||
if j.err != nil {
|
if j.err != nil {
|
||||||
log.Printf("error: '%s' failed: %v\n", j.provider.Name, j.err)
|
p.log.Error("Job execution failed",
|
||||||
|
slog.Group("job",
|
||||||
|
slog.Group("provider"),
|
||||||
|
"name", j.provider.Name,
|
||||||
|
),
|
||||||
|
"err", j.err,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if j.aggregatorProvider == nil {
|
if j.aggregatorProvider == nil {
|
||||||
log.Printf(
|
p.log.Error("Job did not produce any result",
|
||||||
"error: '%s' does not produce any result.\n", j.provider.Name)
|
slog.Group("job",
|
||||||
|
slog.Group("provider"),
|
||||||
|
"name", j.provider.Name,
|
||||||
|
),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
@ -377,7 +376,7 @@ func (w *worker) writeIndices() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for label, summaries := range w.summaries {
|
for label, summaries := range w.summaries {
|
||||||
log.Printf("%s: %d\n", label, len(summaries))
|
w.log.Debug("Writing indices", "label", label, "summaries.num", len(summaries))
|
||||||
if err := w.writeInterims(label, summaries); err != nil {
|
if err := w.writeInterims(label, summaries); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -102,12 +101,12 @@ func (w *worker) checkInterims(
|
||||||
|
|
||||||
// XXX: Should we return an error here?
|
// XXX: Should we return an error here?
|
||||||
for _, e := range errors {
|
for _, e := range errors {
|
||||||
log.Printf("validation error: %s: %v\n", url, e)
|
w.log.Error("validation error", "url", url, "err", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to write the changed content.
|
// We need to write the changed content.
|
||||||
|
|
||||||
// This will start the transcation if not already started.
|
// This will start the transaction if not already started.
|
||||||
dst, err := tx.Dst()
|
dst, err := tx.Dst()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -159,8 +158,7 @@ func (w *worker) checkInterims(
|
||||||
|
|
||||||
// setupProviderInterim prepares the worker for a specific provider.
|
// setupProviderInterim prepares the worker for a specific provider.
|
||||||
func (w *worker) setupProviderInterim(provider *provider) {
|
func (w *worker) setupProviderInterim(provider *provider) {
|
||||||
log.Printf("worker #%d: %s (%s)\n",
|
w.log.Info("Setting up worker", provider.Name, provider.Domain)
|
||||||
w.num, provider.Name, provider.Domain)
|
|
||||||
|
|
||||||
w.dir = ""
|
w.dir = ""
|
||||||
w.provider = provider
|
w.provider = provider
|
||||||
|
|
@ -262,7 +260,7 @@ func (p *processor) interim() error {
|
||||||
queue := make(chan *interimJob)
|
queue := make(chan *interimJob)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
log.Printf("Starting %d workers.\n", p.cfg.Workers)
|
p.log.Info("Starting workers...", "num", p.cfg.Workers)
|
||||||
for i := 1; i <= p.cfg.Workers; i++ {
|
for i := 1; i <= p.cfg.Workers; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
w := newWorker(i, p)
|
w := newWorker(i, p)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
|
@ -85,7 +85,8 @@ func (lt *lazyTransaction) commit() error {
|
||||||
os.RemoveAll(lt.dst)
|
os.RemoveAll(lt.dst)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("Move %q -> %q\n", symlink, lt.src)
|
|
||||||
|
slog.Debug("Moving directory", "from", symlink, "to", lt.src)
|
||||||
if err := os.Rename(symlink, lt.src); err != nil {
|
if err := os.Rename(symlink, lt.src); err != nil {
|
||||||
os.RemoveAll(lt.dst)
|
os.RemoveAll(lt.dst)
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,12 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/csaf-poc/csaf_distribution/v3/internal/options"
|
"github.com/csaf-poc/csaf_distribution/v3/internal/options"
|
||||||
|
|
||||||
"github.com/gofrs/flock"
|
"github.com/gofrs/flock"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -44,8 +46,9 @@ func lock(lockFile *string, fn func() error) error {
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
_, cfg, err := parseArgsConfig()
|
_, cfg, err := parseArgsConfig()
|
||||||
options.ErrorCheck(err)
|
cfg.prepareLogging()
|
||||||
options.ErrorCheck(cfg.prepare())
|
options.ErrorCheckStructured(err)
|
||||||
p := processor{cfg: cfg}
|
options.ErrorCheckStructured(cfg.prepare())
|
||||||
options.ErrorCheck(lock(cfg.LockFile, p.process))
|
p := processor{cfg: cfg, log: slog.Default()}
|
||||||
|
options.ErrorCheckStructured(lock(cfg.LockFile, p.process))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -47,7 +47,7 @@ func (w *worker) mirror() (*csaf.AggregatorCSAFProvider, error) {
|
||||||
if err != nil && w.dir != "" {
|
if err != nil && w.dir != "" {
|
||||||
// If something goes wrong remove the debris.
|
// If something goes wrong remove the debris.
|
||||||
if err := os.RemoveAll(w.dir); err != nil {
|
if err := os.RemoveAll(w.dir); err != nil {
|
||||||
log.Printf("error: %v\n", err)
|
w.log.Error("Could not remove directory", "path", w.dir, "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result, err
|
return result, err
|
||||||
|
|
@ -166,7 +166,7 @@ func (w *worker) writeProviderMetadata() error {
|
||||||
{Expr: `$.public_openpgp_keys`, Action: util.ReMarshalMatcher(&pm.PGPKeys)},
|
{Expr: `$.public_openpgp_keys`, Action: util.ReMarshalMatcher(&pm.PGPKeys)},
|
||||||
}, w.metadataProvider); err != nil {
|
}, w.metadataProvider); err != nil {
|
||||||
// only log the errors
|
// only log the errors
|
||||||
log.Printf("extracting data from orignal provider failed: %v\n", err)
|
w.log.Error("Extracting data from original provider failed", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are mirroring the remote public keys, too.
|
// We are mirroring the remote public keys, too.
|
||||||
|
|
@ -196,11 +196,11 @@ func (w *worker) mirrorPGPKeys(pm *csaf.ProviderMetadata) error {
|
||||||
for i := range pm.PGPKeys {
|
for i := range pm.PGPKeys {
|
||||||
pgpKey := &pm.PGPKeys[i]
|
pgpKey := &pm.PGPKeys[i]
|
||||||
if pgpKey.URL == nil {
|
if pgpKey.URL == nil {
|
||||||
log.Printf("ignoring PGP key without URL: %s\n", pgpKey.Fingerprint)
|
w.log.Warn("Ignoring PGP key without URL", "fingerprint", pgpKey.Fingerprint)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err := hex.DecodeString(string(pgpKey.Fingerprint)); err != nil {
|
if _, err := hex.DecodeString(string(pgpKey.Fingerprint)); err != nil {
|
||||||
log.Printf("ignoring PGP with invalid fingerprint: %s\n", *pgpKey.URL)
|
w.log.Warn("Ignoring PGP key with invalid fingerprint", "url", *pgpKey.URL)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -344,7 +344,7 @@ func (w *worker) doMirrorTransaction() error {
|
||||||
|
|
||||||
// Check if there is a sysmlink already.
|
// Check if there is a sysmlink already.
|
||||||
target := filepath.Join(w.processor.cfg.Folder, w.provider.Name)
|
target := filepath.Join(w.processor.cfg.Folder, w.provider.Name)
|
||||||
log.Printf("target: '%s'\n", target)
|
w.log.Debug("Checking for path existance", "path", target)
|
||||||
|
|
||||||
exists, err := util.PathExists(target)
|
exists, err := util.PathExists(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -359,7 +359,7 @@ func (w *worker) doMirrorTransaction() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("sym link: %s -> %s\n", w.dir, target)
|
w.log.Debug("Creating sym link", "from", w.dir, "to", target)
|
||||||
|
|
||||||
// Create a new symlink
|
// Create a new symlink
|
||||||
if err := os.Symlink(w.dir, target); err != nil {
|
if err := os.Symlink(w.dir, target); err != nil {
|
||||||
|
|
@ -368,7 +368,7 @@ func (w *worker) doMirrorTransaction() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move the symlink
|
// Move the symlink
|
||||||
log.Printf("Move: %s -> %s\n", target, webTarget)
|
w.log.Debug("Moving sym link", "from", target, "to", webTarget)
|
||||||
if err := os.Rename(target, webTarget); err != nil {
|
if err := os.Rename(target, webTarget); err != nil {
|
||||||
os.RemoveAll(w.dir)
|
os.RemoveAll(w.dir)
|
||||||
return err
|
return err
|
||||||
|
|
@ -499,14 +499,14 @@ func (w *worker) mirrorFiles(tlpLabel csaf.TLPLabel, files []csaf.AdvisoryFile)
|
||||||
|
|
||||||
u, err := url.Parse(file.URL())
|
u, err := url.Parse(file.URL())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error: %s\n", err)
|
w.log.Error("Could not parse advisory file URL", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should we ignore this advisory?
|
// Should we ignore this advisory?
|
||||||
if w.provider.ignoreURL(file.URL(), w.processor.cfg) {
|
if w.provider.ignoreURL(file.URL(), w.processor.cfg) {
|
||||||
if w.processor.cfg.Verbose {
|
if w.processor.cfg.Verbose {
|
||||||
log.Printf("Ignoring %s: %q\n", w.provider.Name, file.URL())
|
w.log.Info("Ignoring advisory", slog.Group("provider", "name", w.provider.Name), "file", file)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -514,7 +514,7 @@ func (w *worker) mirrorFiles(tlpLabel csaf.TLPLabel, files []csaf.AdvisoryFile)
|
||||||
// Ignore not conforming filenames.
|
// Ignore not conforming filenames.
|
||||||
filename := filepath.Base(u.Path)
|
filename := filepath.Base(u.Path)
|
||||||
if !util.ConformingFileName(filename) {
|
if !util.ConformingFileName(filename) {
|
||||||
log.Printf("Not conforming filename %q. Ignoring.\n", filename)
|
w.log.Warn("Ignoring advisory because of non-conforming filename", "filename", filename)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -531,19 +531,18 @@ func (w *worker) mirrorFiles(tlpLabel csaf.TLPLabel, files []csaf.AdvisoryFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := downloadJSON(w.client, file.URL(), download); err != nil {
|
if err := downloadJSON(w.client, file.URL(), download); err != nil {
|
||||||
log.Printf("error: %v\n", err)
|
w.log.Error("Error while downloading JSON", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check against CSAF schema.
|
// Check against CSAF schema.
|
||||||
errors, err := csaf.ValidateCSAF(advisory)
|
errors, err := csaf.ValidateCSAF(advisory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error: %s: %v", file, err)
|
w.log.Error("Error while validating CSAF schema", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(errors) > 0 {
|
if len(errors) > 0 {
|
||||||
log.Printf("CSAF file %s has %d validation errors.\n",
|
w.log.Error("CSAF file has validation errors", "num.errors", len(errors), "file", file)
|
||||||
file, len(errors))
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -551,29 +550,27 @@ func (w *worker) mirrorFiles(tlpLabel csaf.TLPLabel, files []csaf.AdvisoryFile)
|
||||||
if rmv := w.processor.remoteValidator; rmv != nil {
|
if rmv := w.processor.remoteValidator; rmv != nil {
|
||||||
rvr, err := rmv.Validate(advisory)
|
rvr, err := rmv.Validate(advisory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Calling remote validator failed: %s\n", err)
|
w.log.Error("Calling remote validator failed", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !rvr.Valid {
|
if !rvr.Valid {
|
||||||
log.Printf(
|
w.log.Error("CSAF file does not validate remotely", "file", file.URL())
|
||||||
"CSAF file %s does not validate remotely.\n", file)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sum, err := csaf.NewAdvisorySummary(w.expr, advisory)
|
sum, err := csaf.NewAdvisorySummary(w.expr, advisory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error: %s: %v\n", file, err)
|
w.log.Error("Error while creating new advisory", "file", file, "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if util.CleanFileName(sum.ID) != filename {
|
if util.CleanFileName(sum.ID) != filename {
|
||||||
log.Printf("ID %q does not match filename %s",
|
w.log.Error("ID mismatch", "id", sum.ID, "filename", filename)
|
||||||
sum.ID, filename)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.extractCategories(label, advisory); err != nil {
|
if err := w.extractCategories(label, advisory); err != nil {
|
||||||
log.Printf("error: %s: %v\n", file, err)
|
w.log.Error("Could not extract categories", "file", file, "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -624,7 +621,7 @@ func (w *worker) downloadSignatureOrSign(url, fname string, data []byte) error {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != errNotFound {
|
if err != errNotFound {
|
||||||
log.Printf("error: %s: %v\n", url, err)
|
w.log.Error("Could not find signature URL", "url", url, "err", err)
|
||||||
}
|
}
|
||||||
// Sign it our self.
|
// Sign it our self.
|
||||||
if sig, err = w.sign(data); err != nil {
|
if sig, err = w.sign(data); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
|
||||||
|
|
||||||
"github.com/csaf-poc/csaf_distribution/v3/csaf"
|
"github.com/csaf-poc/csaf_distribution/v3/csaf"
|
||||||
"github.com/csaf-poc/csaf_distribution/v3/util"
|
"github.com/csaf-poc/csaf_distribution/v3/util"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type processor struct {
|
type processor struct {
|
||||||
|
|
@ -26,6 +26,9 @@ type processor struct {
|
||||||
|
|
||||||
// remoteValidator is a globally configured remote validator.
|
// remoteValidator is a globally configured remote validator.
|
||||||
remoteValidator csaf.RemoteValidator
|
remoteValidator csaf.RemoteValidator
|
||||||
|
|
||||||
|
// log is the structured logger for the whole processor.
|
||||||
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type summary struct {
|
type summary struct {
|
||||||
|
|
@ -48,6 +51,7 @@ type worker struct {
|
||||||
dir string // Directory to store data to.
|
dir string // Directory to store data to.
|
||||||
summaries map[string][]summary // the summaries of the advisories.
|
summaries map[string][]summary // the summaries of the advisories.
|
||||||
categories map[string]util.Set[string] // the categories per label.
|
categories map[string]util.Set[string] // the categories per label.
|
||||||
|
log *slog.Logger // the structured logger, supplied with the worker number.
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWorker(num int, processor *processor) *worker {
|
func newWorker(num int, processor *processor) *worker {
|
||||||
|
|
@ -55,6 +59,7 @@ func newWorker(num int, processor *processor) *worker {
|
||||||
num: num,
|
num: num,
|
||||||
processor: processor,
|
processor: processor,
|
||||||
expr: util.NewPathEval(),
|
expr: util.NewPathEval(),
|
||||||
|
log: processor.log.With(slog.Int("worker", num)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,9 +91,10 @@ func (w *worker) locateProviderMetadata(domain string) error {
|
||||||
|
|
||||||
if w.processor.cfg.Verbose {
|
if w.processor.cfg.Verbose {
|
||||||
for i := range lpmd.Messages {
|
for i := range lpmd.Messages {
|
||||||
log.Printf(
|
w.log.Info(
|
||||||
"Loading provider-metadata.json of %q: %s\n",
|
"Loading provider-metadata.json",
|
||||||
domain, lpmd.Messages[i].Message)
|
"domain", domain,
|
||||||
|
"message", lpmd.Messages[i].Message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,7 +147,7 @@ func (p *processor) removeOrphans() error {
|
||||||
|
|
||||||
fi, err := entry.Info()
|
fi, err := entry.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error: %v\n", err)
|
p.log.Error("Could not retrieve file info", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,13 +159,13 @@ func (p *processor) removeOrphans() error {
|
||||||
d := filepath.Join(path, entry.Name())
|
d := filepath.Join(path, entry.Name())
|
||||||
r, err := filepath.EvalSymlinks(d)
|
r, err := filepath.EvalSymlinks(d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error: %v\n", err)
|
p.log.Error("Could not evaluate symlink", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fd, err := os.Stat(r)
|
fd, err := os.Stat(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error: %v\n", err)
|
p.log.Error("Could not retrieve file stats", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,18 +175,18 @@ func (p *processor) removeOrphans() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the link.
|
// Remove the link.
|
||||||
log.Printf("removing link %s -> %s\n", d, r)
|
p.log.Info("Removing link", "path", fmt.Sprintf("%s -> %s", d, r))
|
||||||
if err := os.Remove(d); err != nil {
|
if err := os.Remove(d); err != nil {
|
||||||
log.Printf("error: %v\n", err)
|
p.log.Error("Could not remove symlink", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only remove directories which are in our folder.
|
// Only remove directories which are in our folder.
|
||||||
if rel, err := filepath.Rel(prefix, r); err == nil &&
|
if rel, err := filepath.Rel(prefix, r); err == nil &&
|
||||||
rel == filepath.Base(r) {
|
rel == filepath.Base(r) {
|
||||||
log.Printf("removing directory %s\n", r)
|
p.log.Info("Remove directory", "path", r)
|
||||||
if err := os.RemoveAll(r); err != nil {
|
if err := os.RemoveAll(r); err != nil {
|
||||||
log.Printf("error: %v\n", err)
|
p.log.Error("Could not remove directory", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,12 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
|
|
||||||
"github.com/csaf-poc/csaf_distribution/v3/internal/certs"
|
"github.com/csaf-poc/csaf_distribution/v3/internal/certs"
|
||||||
"github.com/csaf-poc/csaf_distribution/v3/internal/filter"
|
"github.com/csaf-poc/csaf_distribution/v3/internal/filter"
|
||||||
"github.com/csaf-poc/csaf_distribution/v3/internal/models"
|
"github.com/csaf-poc/csaf_distribution/v3/internal/models"
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -29,8 +30,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,13 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
|
|
||||||
"github.com/csaf-poc/csaf_distribution/v3/internal/misc"
|
"github.com/csaf-poc/csaf_distribution/v3/internal/misc"
|
||||||
"github.com/csaf-poc/csaf_distribution/v3/util"
|
"github.com/csaf-poc/csaf_distribution/v3/util"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"mime"
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -22,8 +23,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
|
|
||||||
"github.com/csaf-poc/csaf_distribution/v3/internal/options"
|
"github.com/csaf-poc/csaf_distribution/v3/internal/options"
|
||||||
"github.com/csaf-poc/csaf_distribution/v3/util"
|
"github.com/csaf-poc/csaf_distribution/v3/util"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,10 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
|
|
||||||
"github.com/csaf-poc/csaf_distribution/v3/internal/options"
|
"github.com/csaf-poc/csaf_distribution/v3/internal/options"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "golang.org/x/exp/slog"
|
import "log/slog"
|
||||||
|
|
||||||
// stats contains counters of the downloads.
|
// stats contains counters of the downloads.
|
||||||
type stats struct {
|
type stats struct {
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,8 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStatsAdd(t *testing.T) {
|
func TestStatsAdd(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -23,6 +24,7 @@ import (
|
||||||
|
|
||||||
// AdvisoryFile constructs the urls of a remote file.
|
// AdvisoryFile constructs the urls of a remote file.
|
||||||
type AdvisoryFile interface {
|
type AdvisoryFile interface {
|
||||||
|
slog.LogValuer
|
||||||
URL() string
|
URL() string
|
||||||
SHA256URL() string
|
SHA256URL() string
|
||||||
SHA512URL() string
|
SHA512URL() string
|
||||||
|
|
@ -46,6 +48,11 @@ func (paf PlainAdvisoryFile) SHA512URL() string { return string(paf) + ".sha512"
|
||||||
// SignURL returns the URL of signature file of this advisory.
|
// SignURL returns the URL of signature file of this advisory.
|
||||||
func (paf PlainAdvisoryFile) SignURL() string { return string(paf) + ".asc" }
|
func (paf PlainAdvisoryFile) SignURL() string { return string(paf) + ".asc" }
|
||||||
|
|
||||||
|
// LogValue implements [slog.LogValuer]
|
||||||
|
func (paf PlainAdvisoryFile) LogValue() slog.Value {
|
||||||
|
return slog.GroupValue(slog.String("url", paf.URL()))
|
||||||
|
}
|
||||||
|
|
||||||
// HashedAdvisoryFile is a more involed version of checkFile.
|
// HashedAdvisoryFile is a more involed version of checkFile.
|
||||||
// Here each component can be given explicitly.
|
// Here each component can be given explicitly.
|
||||||
// If a component is not given it is constructed by
|
// If a component is not given it is constructed by
|
||||||
|
|
@ -71,6 +78,11 @@ func (haf HashedAdvisoryFile) SHA512URL() string { return haf.name(2, ".sha512")
|
||||||
// SignURL returns the URL of signature file of this advisory.
|
// SignURL returns the URL of signature file of this advisory.
|
||||||
func (haf HashedAdvisoryFile) SignURL() string { return haf.name(3, ".asc") }
|
func (haf HashedAdvisoryFile) SignURL() string { return haf.name(3, ".asc") }
|
||||||
|
|
||||||
|
// LogValue implements [slog.LogValuer]
|
||||||
|
func (haf HashedAdvisoryFile) LogValue() slog.Value {
|
||||||
|
return slog.GroupValue(slog.String("url", haf.URL()))
|
||||||
|
}
|
||||||
|
|
||||||
// AdvisoryFileProcessor implements the extraction of
|
// AdvisoryFileProcessor implements the extraction of
|
||||||
// advisory file names from a given provider metadata.
|
// advisory file names from a given provider metadata.
|
||||||
type AdvisoryFileProcessor struct {
|
type AdvisoryFileProcessor struct {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
## Supported Go versions
|
## Supported Go versions
|
||||||
|
|
||||||
We support the latest version and the one before
|
We support the latest version and the one before
|
||||||
the latest version of Go (currently 1.21 and 1.20).
|
the latest version of Go (currently 1.22 and 1.21).
|
||||||
|
|
||||||
## Generated files
|
## Generated files
|
||||||
|
|
||||||
|
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -1,6 +1,6 @@
|
||||||
module github.com/csaf-poc/csaf_distribution/v3
|
module github.com/csaf-poc/csaf_distribution/v3
|
||||||
|
|
||||||
go 1.20
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.3.2
|
github.com/BurntSushi/toml v1.3.2
|
||||||
|
|
@ -14,7 +14,6 @@ require (
|
||||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
||||||
go.etcd.io/bbolt v1.3.8
|
go.etcd.io/bbolt v1.3.8
|
||||||
golang.org/x/crypto v0.14.0
|
golang.org/x/crypto v0.14.0
|
||||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
|
|
||||||
golang.org/x/term v0.13.0
|
golang.org/x/term v0.13.0
|
||||||
golang.org/x/time v0.3.0
|
golang.org/x/time v0.3.0
|
||||||
)
|
)
|
||||||
|
|
|
||||||
3
go.sum
3
go.sum
|
|
@ -42,6 +42,7 @@ github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
||||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||||
|
|
@ -51,8 +52,6 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
|
||||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,8 @@
|
||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// LogLevel implements a helper type to be used in configurations.
|
// LogLevel implements a helper type to be used in configurations.
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,8 @@
|
||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMarshalFlag(t *testing.T) {
|
func TestMarshalFlag(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,14 @@ package options
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/csaf-poc/csaf_distribution/v3/util"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
"github.com/jessevdk/go-flags"
|
"github.com/jessevdk/go-flags"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
|
|
||||||
"github.com/csaf-poc/csaf_distribution/v3/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parser helps parsing command line arguments and loading
|
// Parser helps parsing command line arguments and loading
|
||||||
|
|
@ -147,3 +148,13 @@ func ErrorCheck(err error) {
|
||||||
log.Fatalf("error: %v\n", err)
|
log.Fatalf("error: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrorCheckStructured checks if err is not nil and terminates the program if
|
||||||
|
// so. This is similar to [ErrorCheck], but uses [slog] instead of the
|
||||||
|
// non-structured Go logging.
|
||||||
|
func ErrorCheckStructured(err error) {
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error while executing program", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue