From 565238da9a3707c9088806e16b7881c1de4aefd9 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Sun, 5 Dec 2021 15:20:50 +0100 Subject: [PATCH] Separated result rendering from controller actions. --- cmd/csaf_provider/actions.go | 316 +++++++++++++++++++++++++++ cmd/csaf_provider/controller.go | 335 ++--------------------------- cmd/csaf_provider/tmpl/create.html | 13 +- 3 files changed, 343 insertions(+), 321 deletions(-) create mode 100644 cmd/csaf_provider/actions.go diff --git a/cmd/csaf_provider/actions.go b/cmd/csaf_provider/actions.go new file mode 100644 index 0000000..13ba7be --- /dev/null +++ b/cmd/csaf_provider/actions.go @@ -0,0 +1,316 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/csaf-poc/csaf_distribution/csaf" +) + +const dateFormat = time.RFC3339 + +func cleanFileName(s string) string { + s = strings.ReplaceAll(s, `/`, ``) + s = strings.ReplaceAll(s, `\`, ``) + r := regexp.MustCompile(`\.{2,}`) + s = r.ReplaceAllString(s, `.`) + return s +} + +func loadCSAF(r *http.Request) (string, []byte, error) { + file, handler, err := r.FormFile("csaf") + if err != nil { + return "", nil, err + } + defer file.Close() + + var buf bytes.Buffer + lr := io.LimitReader(file, 10*1024*1024) + if _, err := io.Copy(&buf, lr); err != nil { + return "", nil, err + } + return cleanFileName(handler.Filename), buf.Bytes(), nil +} + +func (c *controller) handleSignature( + r *http.Request, + data []byte, +) (string, *crypto.Key, error) { + + // Either way ... we need the key. + key, err := c.cfg.loadCryptoKey() + if err != nil { + return "", nil, err + } + + // Was the signature given via request? + if c.cfg.UploadSignature { + sigText := r.FormValue("signature") + if sigText == "" { + return "", nil, errors.New("missing signature in request") + } + + pgpSig, err := crypto.NewPGPSignatureFromArmored(sigText) + if err != nil { + return "", nil, err + } + + // Use as public key + signRing, err := crypto.NewKeyRing(key) + if err != nil { + return "", nil, err + } + + if err := signRing.VerifyDetached( + crypto.NewPlainMessage(data), + pgpSig, crypto.GetUnixTime(), + ); err != nil { + return "", nil, err + } + + return sigText, key, nil + } + + // Sign ourself + + if passwd := r.FormValue("passphrase"); !c.cfg.NoPassphrase && passwd != "" { + if key, err = key.Unlock([]byte(passwd)); err != nil { + return "", nil, err + } + } + + // Use as private key + signRing, err := crypto.NewKeyRing(key) + if err != nil { + return "", nil, err + } + + sig, err := signRing.SignDetached(crypto.NewPlainMessage(data)) + if err != nil { + return "", nil, err + } + + armored, err := sig.GetArmored() + return armored, key, err +} + +func (c *controller) tlpParam(r *http.Request) (tlp, error) { + t := tlp(strings.ToLower(r.FormValue("tlp"))) + for _, x := range c.cfg.TLPs { + if x == t { + return t, nil + } + } + return "", fmt.Errorf("unsupported TLP type '%s'", t) +} + +func (c *controller) create(http.ResponseWriter, *http.Request) error { + return ensureFolders(c.cfg) +} + +func (c *controller) upload(rw http.ResponseWriter, r *http.Request) (interface{}, error) { + + newCSAF, data, err := loadCSAF(r) + if err != nil { + return nil, err + } + + var content interface{} + if err := json.Unmarshal(data, &content); err != nil { + return nil, err + } + + // Validate againt JSON schema. + if !c.cfg.NoValidation { + validationErrors, err := csaf.ValidateCSAF(content) + if err != nil { + return nil, err + } + + if len(validationErrors) > 0 { + return nil, multiError(validationErrors) + } + } + + ex, err := newExtraction(content) + if err != nil { + return nil, err + } + + t, err := c.tlpParam(r) + if err != nil { + return nil, err + } + + // Extract real TLP from document. + if t == tlpCSAF { + if t = tlp(strings.ToLower(ex.tlpLabel)); !t.valid() || t == tlpCSAF { + return nil, fmt.Errorf("not a valid TL: %s", ex.tlpLabel) + } + } + + armored, key, err := c.handleSignature(r, data) + if err != nil { + return nil, err + } + + var warnings []string + warn := func(msg string) { warnings = append(warnings, msg) } + + if err := doTransaction( + c.cfg, t, + func(folder string, pmd *csaf.ProviderMetadata) error { + + // Load the feed + ts := string(t) + feedName := "csaf-feed-tlp-" + ts + ".json" + + feed := filepath.Join(folder, feedName) + var rolie *csaf.ROLIEFeed + if err := func() error { + f, err := os.Open(feed) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + defer f.Close() + rolie, err = csaf.LoadROLIEFeed(f) + return err + }(); err != nil { + return err + } + + feedURL := csaf.JSONURL( + c.cfg.Domain + "/.well-known/csaf/" + ts + "/" + feedName) + + tlpLabel := csaf.TLPLabel(strings.ToUpper(ts)) + + // Create new if does not exists. + if rolie == nil { + rolie = &csaf.ROLIEFeed{ + ID: "csaf-feed-tlp-" + ts, + Title: "CSAF feed (TLP:" + string(tlpLabel) + ")", + Link: []csaf.Link{{ + Rel: "rel", + HRef: string(feedURL), + }}, + } + } + + rolie.Updated = csaf.TimeStamp(time.Now()) + + year := strconv.Itoa(ex.currentReleaseDate.Year()) + + csafURL := c.cfg.Domain + + "/.well-known/csaf/" + ts + "/" + year + "/" + newCSAF + + e := rolie.EntryByID(ex.id) + if e == nil { + e = &csaf.Entry{ID: ex.id} + rolie.Entry = append(rolie.Entry, e) + } + + e.Titel = ex.title + e.Published = csaf.TimeStamp(ex.initialReleaseDate) + e.Updated = csaf.TimeStamp(ex.currentReleaseDate) + e.Link = []csaf.Link{{ + Rel: "self", + HRef: csafURL, + }} + e.Format = csaf.Format{ + Schema: "https://docs.oasis-open.org/csaf/csaf/v2.0/csaf_json_schema.json", + Version: "2.0", + } + e.Content = csaf.Content{ + Type: "application/json", + Src: csafURL, + } + if ex.summary != "" { + e.Summary = &csaf.Summary{Content: ex.summary} + } else { + e.Summary = nil + } + + // Sort by descending updated order. + rolie.SortEntriesByUpdated() + + // Store the feed + if err := saveToFile(feed, rolie); err != nil { + return err + } + + // Create yearly subfolder + + subDir := filepath.Join(folder, year) + + // Create folder if it does not exists. + if _, err := os.Stat(subDir); err != nil { + if os.IsNotExist(err) { + if err := os.Mkdir(subDir, 0755); err != nil { + return err + } + } else { + return err + } + } + + fname := filepath.Join(subDir, newCSAF) + + if err := writeHashedFile(fname, newCSAF, data, armored); err != nil { + return err + } + + if err := updateIndices( + folder, filepath.Join(year, newCSAF), + ex.currentReleaseDate, + ); err != nil { + return err + } + + // Take over publisher + switch { + case pmd.Publisher == nil: + warn("Publisher in provider metadata is not initialized. Forgot to configure?") + if c.cfg.DynamicProviderMetaData { + warn("Taking publisher from CSAF") + pmd.Publisher = ex.publisher + } + case !pmd.Publisher.Equals(ex.publisher): + warn("Publishers in provider metadata and CSAF do not match.") + } + + keyID, fingerprint := key.GetHexKeyID(), key.GetFingerprint() + pmd.SetPGP(fingerprint, c.cfg.GetOpenPGPURL(keyID)) + + return nil + }, + ); err != nil { + return nil, err + } + + result := struct { + Name string `json:"name"` + ReleaseDate string `json:"release_date"` + Warnings []string `json:"warnings,omitempty"` + Error error `json:"-"` + }{ + Name: newCSAF, + ReleaseDate: ex.currentReleaseDate.Format(dateFormat), + Warnings: warnings, + } + + return &result, nil +} diff --git a/cmd/csaf_provider/controller.go b/cmd/csaf_provider/controller.go index 91e4d08..e90b09c 100644 --- a/cmd/csaf_provider/controller.go +++ b/cmd/csaf_provider/controller.go @@ -1,32 +1,22 @@ package main import ( - "bytes" "embed" - "encoding/json" - "errors" - "fmt" "html/template" - "io" "log" "net/http" - "os" - "path/filepath" - "regexp" - "strconv" "strings" - "time" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - - "github.com/csaf-poc/csaf_distribution/csaf" ) -const dateFormat = time.RFC3339 - //go:embed tmpl var tmplFS embed.FS +type multiError []string + +func (me multiError) Error() string { + return strings.Join([]string(me), ", ") +} + type controller struct { cfg *config tmpl *template.Template @@ -46,8 +36,8 @@ func newController(cfg *config) (*controller, error) { func (c *controller) bind(pim *pathInfoMux) { pim.handleFunc("/", c.index) - pim.handleFunc("/upload", c.upload) - pim.handleFunc("/create", c.create) + pim.handleFunc("/upload", c.uploadWeb) + pim.handleFunc("/create", c.createWeb) } func (c *controller) render(rw http.ResponseWriter, tmpl string, arg interface{}) { @@ -58,19 +48,11 @@ func (c *controller) render(rw http.ResponseWriter, tmpl string, arg interface{} } func (c *controller) failed(rw http.ResponseWriter, tmpl string, err error) { - rw.Header().Set("Content-type", "text/html; charset=utf-8") - result := map[string]interface{}{"Error": []error{err}} - if err := c.tmpl.ExecuteTemplate(rw, tmpl, result); err != nil { - log.Printf("warn: %v\n", err) + if _, ok := err.(multiError); err != nil && !ok { + err = multiError([]string{err.Error()}) } -} - -func (c *controller) multiFailed(rw http.ResponseWriter, tmpl string, err interface{}) { - rw.Header().Set("Content-type", "text/html; charset=utf-8") result := map[string]interface{}{"Error": err} - if err := c.tmpl.ExecuteTemplate(rw, tmpl, result); err != nil { - log.Printf("warn: %v\n", err) - } + c.render(rw, tmpl, result) } func (c *controller) index(rw http.ResponseWriter, r *http.Request) { @@ -79,306 +61,19 @@ func (c *controller) index(rw http.ResponseWriter, r *http.Request) { }) } -func (c *controller) create(rw http.ResponseWriter, r *http.Request) { - if err := ensureFolders(c.cfg); err != nil { +func (c *controller) createWeb(rw http.ResponseWriter, r *http.Request) { + if err := c.create(rw, r); err != nil { c.failed(rw, "create.html", err) return } c.render(rw, "create.html", nil) } -func (c *controller) tlpParam(r *http.Request) (tlp, error) { - t := tlp(strings.ToLower(r.FormValue("tlp"))) - for _, x := range c.cfg.TLPs { - if x == t { - return t, nil - } - } - return "", fmt.Errorf("unsupported TLP type '%s'", t) -} - -func cleanFileName(s string) string { - s = strings.ReplaceAll(s, `/`, ``) - s = strings.ReplaceAll(s, `\`, ``) - r := regexp.MustCompile(`\.{2,}`) - s = r.ReplaceAllString(s, `.`) - return s -} - -func loadCSAF(r *http.Request) (string, []byte, error) { - file, handler, err := r.FormFile("csaf") - if err != nil { - return "", nil, err - } - defer file.Close() - - var buf bytes.Buffer - lr := io.LimitReader(file, 10*1024*1024) - if _, err := io.Copy(&buf, lr); err != nil { - return "", nil, err - } - return cleanFileName(handler.Filename), buf.Bytes(), nil -} - -func (c *controller) handleSignature( - r *http.Request, - data []byte, -) (string, *crypto.Key, error) { - - // Either way ... we need the key. - key, err := c.cfg.loadCryptoKey() - if err != nil { - return "", nil, err - } - - // Was the signature given via request? - if c.cfg.UploadSignature { - sigText := r.FormValue("signature") - if sigText == "" { - return "", nil, errors.New("missing signature in request") - } - - pgpSig, err := crypto.NewPGPSignatureFromArmored(sigText) - if err != nil { - return "", nil, err - } - - // Use as public key - signRing, err := crypto.NewKeyRing(key) - if err != nil { - return "", nil, err - } - - if err := signRing.VerifyDetached( - crypto.NewPlainMessage(data), - pgpSig, crypto.GetUnixTime(), - ); err != nil { - return "", nil, err - } - - return sigText, key, nil - } - - // Sign ourself - - if passwd := r.FormValue("passphrase"); !c.cfg.NoPassphrase && passwd != "" { - if key, err = key.Unlock([]byte(passwd)); err != nil { - return "", nil, err - } - } - - // Use as private key - signRing, err := crypto.NewKeyRing(key) - if err != nil { - return "", nil, err - } - - sig, err := signRing.SignDetached(crypto.NewPlainMessage(data)) - if err != nil { - return "", nil, err - } - - armored, err := sig.GetArmored() - return armored, key, err -} - -func (c *controller) upload(rw http.ResponseWriter, r *http.Request) { - - newCSAF, data, err := loadCSAF(r) +func (c *controller) uploadWeb(rw http.ResponseWriter, r *http.Request) { + result, err := c.upload(rw, r) if err != nil { c.failed(rw, "upload.html", err) return } - - var content interface{} - if err := json.Unmarshal(data, &content); err != nil { - c.failed(rw, "upload.html", err) - return - } - - // Validate againt JSON schema. - if !c.cfg.NoValidation { - validationErrors, err := csaf.ValidateCSAF(content) - if err != nil { - c.failed(rw, "upload.html", err) - return - } - - if len(validationErrors) > 0 { - c.multiFailed(rw, "upload.html", validationErrors) - return - } - } - - ex, err := newExtraction(content) - if err != nil { - c.failed(rw, "upload.html", err) - return - } - - t, err := c.tlpParam(r) - if err != nil { - c.failed(rw, "upload.html", err) - return - } - - // Extract real TLP from document. - if t == tlpCSAF { - if t = tlp(strings.ToLower(ex.tlpLabel)); !t.valid() || t == tlpCSAF { - c.failed( - rw, "upload.html", fmt.Errorf("not a valid TL: %s", ex.tlpLabel)) - return - } - } - - armored, key, err := c.handleSignature(r, data) - if err != nil { - c.failed(rw, "upload.html", err) - return - } - - var warnings []string - warn := func(msg string) { warnings = append(warnings, msg) } - - if err := doTransaction( - c.cfg, t, - func(folder string, pmd *csaf.ProviderMetadata) error { - - // Load the feed - ts := string(t) - feedName := "csaf-feed-tlp-" + ts + ".json" - - feed := filepath.Join(folder, feedName) - var rolie *csaf.ROLIEFeed - if err := func() error { - f, err := os.Open(feed) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return err - } - defer f.Close() - rolie, err = csaf.LoadROLIEFeed(f) - return err - }(); err != nil { - return err - } - - feedURL := csaf.JSONURL( - c.cfg.Domain + "/.well-known/csaf/" + ts + "/" + feedName) - - tlpLabel := csaf.TLPLabel(strings.ToUpper(ts)) - - // Create new if does not exists. - if rolie == nil { - rolie = &csaf.ROLIEFeed{ - ID: "csaf-feed-tlp-" + ts, - Title: "CSAF feed (TLP:" + string(tlpLabel) + ")", - Link: []csaf.Link{{ - Rel: "rel", - HRef: string(feedURL), - }}, - } - } - - rolie.Updated = csaf.TimeStamp(time.Now()) - - year := strconv.Itoa(ex.currentReleaseDate.Year()) - - csafURL := c.cfg.Domain + - "/.well-known/csaf/" + ts + "/" + year + "/" + newCSAF - - e := rolie.EntryByID(ex.id) - if e == nil { - e = &csaf.Entry{ID: ex.id} - rolie.Entry = append(rolie.Entry, e) - } - - e.Titel = ex.title - e.Published = csaf.TimeStamp(ex.initialReleaseDate) - e.Updated = csaf.TimeStamp(ex.currentReleaseDate) - e.Link = []csaf.Link{{ - Rel: "self", - HRef: csafURL, - }} - e.Format = csaf.Format{ - Schema: "https://docs.oasis-open.org/csaf/csaf/v2.0/csaf_json_schema.json", - Version: "2.0", - } - e.Content = csaf.Content{ - Type: "application/json", - Src: csafURL, - } - if ex.summary != "" { - e.Summary = &csaf.Summary{Content: ex.summary} - } else { - e.Summary = nil - } - - // Sort by descending updated order. - rolie.SortEntriesByUpdated() - - // Store the feed - if err := saveToFile(feed, rolie); err != nil { - return err - } - - // Create yearly subfolder - - subDir := filepath.Join(folder, year) - - // Create folder if it does not exists. - if _, err := os.Stat(subDir); err != nil { - if os.IsNotExist(err) { - if err := os.Mkdir(subDir, 0755); err != nil { - return err - } - } else { - return err - } - } - - fname := filepath.Join(subDir, newCSAF) - - if err := writeHashedFile(fname, newCSAF, data, armored); err != nil { - return err - } - - if err := updateIndices( - folder, filepath.Join(year, newCSAF), - ex.currentReleaseDate, - ); err != nil { - return err - } - - // Take over publisher - switch { - case pmd.Publisher == nil: - warn("Publisher in provider metadata is not initialized. Forgot to configure?") - if c.cfg.DynamicProviderMetaData { - warn("Taking publisher from CSAF") - pmd.Publisher = ex.publisher - } - case !pmd.Publisher.Equals(ex.publisher): - warn("Publishers in provider metadata and CSAF do not match.") - } - - keyID, fingerprint := key.GetHexKeyID(), key.GetFingerprint() - pmd.SetPGP(fingerprint, c.cfg.GetOpenPGPURL(keyID)) - - return nil - }, - ); err != nil { - c.failed(rw, "upload.html", err) - return - } - - result := map[string]interface{}{ - "Name": newCSAF, - "ReleaseDate": ex.currentReleaseDate.Format(dateFormat), - "Warnings": warnings, - } - c.render(rw, "upload.html", result) } diff --git a/cmd/csaf_provider/tmpl/create.html b/cmd/csaf_provider/tmpl/create.html index 37bfadc..f2de646 100644 --- a/cmd/csaf_provider/tmpl/create.html +++ b/cmd/csaf_provider/tmpl/create.html @@ -8,7 +8,18 @@

CSAF-Provider - Directory structure created

{{ if .Error }} - Error: {{ .Error }}. + {{ if eq (len .Error) 1 }} + Error: {{ index .Error 0 }}. + {{ else }} +

+ Errors: +

+

+ {{ end }} {{ else }} Everything is setup fine now. {{ end }}