From 9ef3696c6fec0a37f6f869f0de133c64ee1b04d3 Mon Sep 17 00:00:00 2001 From: Olivier Duclos Date: Sat, 15 Jun 2024 20:35:33 +0200 Subject: [PATCH] MEDIUM: Add support for the crt-store section With all its keywords: crt-base, key-base and load. --- init.go | 2 + parser.go | 1 + parsers/crt-store-load.go | 117 +++++++++++++++++ parsers/load_generated.go | 157 +++++++++++++++++++++++ reader.go | 10 ++ section-parsers.go | 10 ++ tests/configs/haproxy.cfg.go | 6 + tests/configs/haproxy_generated.cfg.go | 26 ++++ tests/integration/crt-store_data_test.go | 51 ++++++++ tests/integration/crt-store_test.go | 54 ++++++++ tests/load_generated_test.go | 90 +++++++++++++ types/types.go | 24 ++++ writer.go | 2 +- 13 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 parsers/crt-store-load.go create mode 100644 parsers/load_generated.go create mode 100644 tests/integration/crt-store_data_test.go create mode 100644 tests/integration/crt-store_test.go create mode 100644 tests/load_generated_test.go diff --git a/init.go b/init.go index 0b93c83..f510a2c 100644 --- a/init.go +++ b/init.go @@ -44,6 +44,7 @@ type ConfiguredParsers struct { Ring *Parsers LogForward *Parsers FCGIApp *Parsers + CrtStore *Parsers // spoe parsers SPOEAgent *Parsers SPOEGroup *Parsers @@ -93,4 +94,5 @@ func (p *configParser) initParserMaps() { p.Parsers[Ring] = map[string]*Parsers{} p.Parsers[LogForward] = map[string]*Parsers{} p.Parsers[FCGIApp] = map[string]*Parsers{} + p.Parsers[CrtStore] = map[string]*Parsers{} } diff --git a/parser.go b/parser.go index b48da4c..ea35a44 100644 --- a/parser.go +++ b/parser.go @@ -46,6 +46,7 @@ const ( Ring Section = "ring" LogForward Section = "log-forward" FCGIApp Section = "fcgi-app" + CrtStore Section = "crt-store" // spoe sections SPOEAgent Section = "spoe-agent" SPOEGroup Section = "spoe-group" diff --git a/parsers/crt-store-load.go b/parsers/crt-store-load.go new file mode 100644 index 0000000..583e939 --- /dev/null +++ b/parsers/crt-store-load.go @@ -0,0 +1,117 @@ +/* +Copyright 2024 HAProxy Technologies + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parsers + +import ( + "strings" + + "github.com/haproxytech/config-parser/v5/common" + "github.com/haproxytech/config-parser/v5/errors" + "github.com/haproxytech/config-parser/v5/types" +) + +type LoadCert struct { + data []types.LoadCert + preComments []string // comments that appear before the actual line +} + +func (p *LoadCert) parseError(line string) *errors.ParseError { + return &errors.ParseError{Parser: "LoadCert", Line: line} +} + +func (p *LoadCert) parse(line string, parts []string, comment string) (*types.LoadCert, error) { + if len(parts) < 3 { + return nil, p.parseError(line) + } + if parts[0] != "load" { + return nil, p.parseError(line) + } + + load := new(types.LoadCert) + + for i := 1; i < len(parts); i++ { + element := parts[i] + switch element { + case "crt": + CheckParsePair(parts, &i, &load.Certificate) + case "alias": + CheckParsePair(parts, &i, &load.Alias) + case "key": + CheckParsePair(parts, &i, &load.Key) + case "ocsp": + CheckParsePair(parts, &i, &load.Ocsp) + case "issuer": + CheckParsePair(parts, &i, &load.Issuer) + case "sctl": + CheckParsePair(parts, &i, &load.Sctl) + case "ocsp-update": + i++ + load.OcspUpdate = new(bool) + if parts[i] == "on" { + *load.OcspUpdate = true + } else if parts[i] != "off" { + return nil, p.parseError(line) + } + } + } + load.Comment = comment + + // crt is mandatory + if load.Certificate == "" { + return nil, p.parseError(line) + } + + return load, nil +} + +func (p *LoadCert) Result() ([]common.ReturnResultLine, error) { + if len(p.data) == 0 { + return nil, errors.ErrFetch + } + + result := make([]common.ReturnResultLine, len(p.data)) + sb := new(strings.Builder) + + for i, load := range p.data { + sb.Reset() + sb.WriteString("load") + CheckWritePair(sb, "crt", load.Certificate) + CheckWritePair(sb, "alias", load.Alias) + CheckWritePair(sb, "key", load.Key) + CheckWritePair(sb, "ocsp", load.Ocsp) + CheckWritePair(sb, "issuer", load.Issuer) + CheckWritePair(sb, "sctl", load.Sctl) + CheckWritePair(sb, "ocsp-update", fmtOnOff(load.OcspUpdate)) + + result[i] = common.ReturnResultLine{ + Data: sb.String(), + Comment: load.Comment, + } + } + + return result, nil +} + +func fmtOnOff(b *bool) string { + if b == nil { + return "" + } + if *b { + return "on" + } + return "off" +} diff --git a/parsers/load_generated.go b/parsers/load_generated.go new file mode 100644 index 0000000..10cd720 --- /dev/null +++ b/parsers/load_generated.go @@ -0,0 +1,157 @@ +// Code generated by go generate; DO NOT EDIT. +/* +Copyright 2019 HAProxy Technologies + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package parsers + +import ( + "github.com/haproxytech/config-parser/v5/common" + "github.com/haproxytech/config-parser/v5/errors" + "github.com/haproxytech/config-parser/v5/types" +) + +func (p *LoadCert) Init() { + p.data = []types.LoadCert{} + p.preComments = []string{} +} + +func (p *LoadCert) GetParserName() string { + return "load" +} + +func (p *LoadCert) Get(createIfNotExist bool) (common.ParserData, error) { + if len(p.data) == 0 && !createIfNotExist { + return nil, errors.ErrFetch + } + return p.data, nil +} + +func (p *LoadCert) GetPreComments() ([]string, error) { + return p.preComments, nil +} + +func (p *LoadCert) SetPreComments(preComments []string) { + p.preComments = preComments +} + +func (p *LoadCert) GetOne(index int) (common.ParserData, error) { + if index < 0 || index >= len(p.data) { + return nil, errors.ErrFetch + } + return p.data[index], nil +} + +func (p *LoadCert) Delete(index int) error { + if index < 0 || index >= len(p.data) { + return errors.ErrFetch + } + copy(p.data[index:], p.data[index+1:]) + p.data[len(p.data)-1] = types.LoadCert{} + p.data = p.data[:len(p.data)-1] + return nil +} + +func (p *LoadCert) Insert(data common.ParserData, index int) error { + if data == nil { + return errors.ErrInvalidData + } + switch newValue := data.(type) { + case []types.LoadCert: + p.data = newValue + case *types.LoadCert: + if index > -1 { + if index > len(p.data) { + return errors.ErrIndexOutOfRange + } + p.data = append(p.data, types.LoadCert{}) + copy(p.data[index+1:], p.data[index:]) + p.data[index] = *newValue + } else { + p.data = append(p.data, *newValue) + } + case types.LoadCert: + if index > -1 { + if index > len(p.data) { + return errors.ErrIndexOutOfRange + } + p.data = append(p.data, types.LoadCert{}) + copy(p.data[index+1:], p.data[index:]) + p.data[index] = newValue + } else { + p.data = append(p.data, newValue) + } + default: + return errors.ErrInvalidData + } + return nil +} + +func (p *LoadCert) Set(data common.ParserData, index int) error { + if data == nil { + p.Init() + return nil + } + switch newValue := data.(type) { + case []types.LoadCert: + p.data = newValue + case *types.LoadCert: + if index > -1 && index < len(p.data) { + p.data[index] = *newValue + } else if index == -1 { + p.data = append(p.data, *newValue) + } else { + return errors.ErrIndexOutOfRange + } + case types.LoadCert: + if index > -1 && index < len(p.data) { + p.data[index] = newValue + } else if index == -1 { + p.data = append(p.data, newValue) + } else { + return errors.ErrIndexOutOfRange + } + default: + return errors.ErrInvalidData + } + return nil +} + +func (p *LoadCert) PreParse(line string, parts []string, preComments []string, comment string) (string, error) { + changeState, err := p.Parse(line, parts, comment) + if err == nil && preComments != nil { + p.preComments = append(p.preComments, preComments...) + } + return changeState, err +} + +func (p *LoadCert) Parse(line string, parts []string, comment string) (string, error) { + if parts[0] == "load" { + data, err := p.parse(line, parts, comment) + if err != nil { + if _, ok := err.(*errors.ParseError); ok { + return "", err + } + return "", &errors.ParseError{Parser: "LoadCert", Line: line} + } + p.data = append(p.data, *data) + return "", nil + } + return "", &errors.ParseError{Parser: "LoadCert", Line: line} +} + +func (p *LoadCert) ResultAll() ([]common.ReturnResultLine, []string, error) { + res, err := p.Result() + return res, p.preComments, err +} diff --git a/reader.go b/reader.go index 729aff2..31ee903 100644 --- a/reader.go +++ b/reader.go @@ -342,6 +342,16 @@ func (p *configParser) ProcessLine(line string, parts []string, comment string, if p.Options.Log { p.Options.Logger.Tracef("%log-forward section %s active", p.Options.LogPrefix, data.Name) } + case "crt-store": + parserSectionName := parser.(*extra.Section) //nolint:forcetypeassert + rawData, _ := parserSectionName.Get(false) + data := rawData.(*types.Section) //nolint:forcetypeassert + config.CrtStore = p.getCrtStoreParser() + p.Parsers[CrtStore][data.Name] = config.CrtStore + config.Active = config.CrtStore + if p.Options.Log { + p.Options.Logger.Tracef("%scrt-store section %s active", p.Options.LogPrefix, data.Name) + } case "snippet_beg": config.Previous = config.Active config.Active = &Parsers{ diff --git a/section-parsers.go b/section-parsers.go index 18ffbdb..5a86440 100644 --- a/section-parsers.go +++ b/section-parsers.go @@ -48,6 +48,7 @@ func (p *configParser) createParsers(parser map[string]ParserInterface, sequence addParser(parser, &sequence, &extra.Section{Name: "ring"}) addParser(parser, &sequence, &extra.Section{Name: "log-forward"}) addParser(parser, &sequence, &extra.Section{Name: "fcgi-app"}) + addParser(parser, &sequence, &extra.Section{Name: "crt-store"}) if !p.Options.DisableUnProcessed { addParser(parser, &sequence, &extra.UnProcessed{}) } @@ -938,3 +939,12 @@ func (p *configParser) getLogForwardParser() *Parsers { addParser(parser, &sequence, &simple.Timeout{Name: "client"}) return p.createParsers(parser, sequence) } + +func (p *configParser) getCrtStoreParser() *Parsers { + parser := map[string]ParserInterface{} + sequence := []Section{} + addParser(parser, &sequence, &simple.Word{Name: "crt-base"}) + addParser(parser, &sequence, &simple.Word{Name: "key-base"}) + addParser(parser, &sequence, &parsers.LoadCert{}) + return p.createParsers(parser, sequence) +} diff --git a/tests/configs/haproxy.cfg.go b/tests/configs/haproxy.cfg.go index b6f3960..68a1d0d 100644 --- a/tests/configs/haproxy.cfg.go +++ b/tests/configs/haproxy.cfg.go @@ -166,6 +166,12 @@ cache foobar total-max-size 4 max-age 240 +crt-store tpm2 + crt-base /c + key-base /k + load crt example.com.pem alias example + load crt lol.pem + frontend healthz from A mode http monitor-uri /healthz diff --git a/tests/configs/haproxy_generated.cfg.go b/tests/configs/haproxy_generated.cfg.go index 0e99e07..7f40fea 100644 --- a/tests/configs/haproxy_generated.cfg.go +++ b/tests/configs/haproxy_generated.cfg.go @@ -1062,6 +1062,16 @@ backend test cache test process-vary on +crt-store test + load crt foo.pem + load crt foo.pem alias foo.com + load crt foo.pem alias foo.com key foo.priv.key + load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der + load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem + load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl + load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl ocsp-update on + load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl ocsp-update off + defaults test acl url_stats path_beg /stats acl url_static path_beg -i /static /images /javascript /stylesheets @@ -3962,6 +3972,22 @@ var configTests = []configTest{{` command spoa-mirror --runtime 0 --mirror-url {` http-fail-codes 400-499 -450 +500 `, 1}, {` http-fail-codes 400-408 # comment +`, 1}, + {` load crt foo.pem +`, 1}, + {` load crt foo.pem alias foo.com +`, 1}, + {` load crt foo.pem alias foo.com key foo.priv.key +`, 1}, + {` load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der +`, 1}, + {` load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem +`, 1}, + {` load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl +`, 1}, + {` load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl ocsp-update on +`, 1}, + {` load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl ocsp-update off `, 1}, {` http-request set-map(map.lst) %[src] %[req.hdr(X-Value)] if value `, 3}, diff --git a/tests/integration/crt-store_data_test.go b/tests/integration/crt-store_data_test.go new file mode 100644 index 0000000..98e9ced --- /dev/null +++ b/tests/integration/crt-store_data_test.go @@ -0,0 +1,51 @@ +// Code generated by go generate; DO NOT EDIT. +/* +Copyright 2019 HAProxy Technologies + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration_test + +const crtstore_loadcrtfoopem = ` +crt-store test + load crt foo.pem +` +const crtstore_loadcrtfoopemaliasfoocom = ` +crt-store test + load crt foo.pem alias foo.com +` +const crtstore_loadcrtfoopemaliasfoocomkeyfoopr = ` +crt-store test + load crt foo.pem alias foo.com key foo.priv.key +` +const crtstore_loadcrtfoopemaliasfoocomkeyfoopr_ = ` +crt-store test + load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der +` +const crtstore_loadcrtfoopemaliasfoocomkeyfoopr__ = ` +crt-store test + load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem +` +const crtstore_loadcrtfoopemaliasfoocomkeyfoopr___ = ` +crt-store test + load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl +` +const crtstore_loadcrtfoopemaliasfoocomkeyfoopr____ = ` +crt-store test + load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl ocsp-update on +` +const crtstore_loadcrtfoopemaliasfoocomkeyfoopr_____ = ` +crt-store test + load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl ocsp-update off +` diff --git a/tests/integration/crt-store_test.go b/tests/integration/crt-store_test.go new file mode 100644 index 0000000..995812d --- /dev/null +++ b/tests/integration/crt-store_test.go @@ -0,0 +1,54 @@ +// Code generated by go generate; DO NOT EDIT. +/* +Copyright 2019 HAProxy Technologies + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration_test + +import ( + "bytes" + "testing" + + parser "github.com/haproxytech/config-parser/v5" + "github.com/haproxytech/config-parser/v5/options" +) + +func TestWholeConfigsSectionsCrtstore(t *testing.T) { + t.Parallel() + tests := []struct { + Name, Config string + }{} + for _, config := range tests { + t.Run(config.Name, func(t *testing.T) { + t.Parallel() + var buffer bytes.Buffer + buffer.WriteString(config.Config) + p, err := parser.New(options.Reader(&buffer)) + if err != nil { + t.Fatalf(err.Error()) + } + result := p.String() + if result != config.Config { + compare(t, config.Config, result) + t.Error("======== ORIGINAL =========") + t.Error(config.Config) + t.Error("======== RESULT ===========") + t.Error(result) + t.Error("===========================") + t.Fatalf("configurations does not match") + } + }) + } +} diff --git a/tests/load_generated_test.go b/tests/load_generated_test.go new file mode 100644 index 0000000..b4b1bfc --- /dev/null +++ b/tests/load_generated_test.go @@ -0,0 +1,90 @@ +// Code generated by go generate; DO NOT EDIT. +/* +Copyright 2019 HAProxy Technologies + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tests + +import ( + "fmt" + "strings" + "testing" + + "github.com/haproxytech/config-parser/v5/parsers" +) + +func TestLoadCert(t *testing.T) { + tests := map[string]bool{ + "load crt foo.pem": true, + "load crt foo.pem alias foo.com": true, + "load crt foo.pem alias foo.com key foo.priv.key": true, + "load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der": true, + "load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem": true, + "load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl": true, + "load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl ocsp-update on": true, + "load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl ocsp-update off": true, + "load alias foo.com key foo.priv.key": false, + "load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem ocsp-update lol": false, + "---": false, + "--- ---": false, + } + parser := &parsers.LoadCert{} + for command, shouldPass := range tests { + t.Run(command, func(t *testing.T) { + line := strings.TrimSpace(command) + lines := strings.SplitN(line, "\n", -1) + var err error + parser.Init() + if len(lines) > 1 { + for _, line = range lines { + line = strings.TrimSpace(line) + if err = ProcessLine(line, parser); err != nil { + break + } + } + } else { + err = ProcessLine(line, parser) + } + if shouldPass { + if err != nil { + t.Errorf(err.Error()) + return + } + result, err := parser.Result() + if err != nil { + t.Errorf(err.Error()) + return + } + var returnLine string + if result[0].Comment == "" { + returnLine = result[0].Data + } else { + returnLine = fmt.Sprintf("%s # %s", result[0].Data, result[0].Comment) + } + if command != returnLine { + t.Errorf(fmt.Sprintf("error: has [%s] expects [%s]", returnLine, command)) + } + } else { + if err == nil { + t.Errorf(fmt.Sprintf("error: did not throw error for line [%s]", line)) + } + _, parseErr := parser.Result() + if parseErr == nil { + t.Errorf(fmt.Sprintf("error: did not throw error on result for line [%s]", line)) + } + } + }) + } +} diff --git a/types/types.go b/types/types.go index 7e893d4..2c635f4 100644 --- a/types/types.go +++ b/types/types.go @@ -1630,3 +1630,27 @@ type HTTPErrCodes struct { type HTTPFailCodes struct { StringC } + +//sections:crt-store +//name:load +//is:multiple +//test:ok:load crt foo.pem +//test:ok:load crt foo.pem alias foo.com +//test:ok:load crt foo.pem alias foo.com key foo.priv.key +//test:ok:load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der +//test:ok:load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem +//test:ok:load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl +//test:ok:load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl ocsp-update on +//test:ok:load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem sctl foo.sctl ocsp-update off +//test:fail:load alias foo.com key foo.priv.key +//test:fail:load crt foo.pem alias foo.com key foo.priv.key ocsp foo.ocsp.der issuer foo.issuer.pem ocsp-update lol +type LoadCert struct { + Certificate string + Alias string + Key string + Ocsp string + Issuer string + Sctl string + Comment string + OcspUpdate *bool +} diff --git a/writer.go b/writer.go index a7b2e32..ea83a83 100644 --- a/writer.go +++ b/writer.go @@ -40,7 +40,7 @@ func (p *configParser) String() string { p.writeParsers("", p.Parsers[Comments][CommentsSectionName], &result, false) p.writeParsers("global", p.Parsers[Global][GlobalSectionName], &result, true) - sections := []Section{Defaults, UserList, Peers, Mailers, Resolvers, Cache, Ring, LogForward, HTTPErrors, Frontends, Backends, Listen, Program, FCGIApp} + sections := []Section{Defaults, UserList, Peers, Mailers, Resolvers, Cache, Ring, LogForward, HTTPErrors, CrtStore, Frontends, Backends, Listen, Program, FCGIApp} for _, section := range sections { var sortedSections []string