From 92c9af76ae3567b26ae9344f8ea7e8efdd22c6f4 Mon Sep 17 00:00:00 2001 From: renovate <group_74_bot_81ef8ad8348b11e39acf561a49e87c1c@noreply.git.autistici.org> Date: Sat, 12 Apr 2025 17:53:40 +0000 Subject: [PATCH] Update module github.com/crewjam/saml to v0.5.0 --- go.mod | 6 +- go.sum | 6 + vendor/github.com/beevik/etree/.travis.yml | 14 - vendor/github.com/beevik/etree/CONTRIBUTORS | 4 + vendor/github.com/beevik/etree/LICENSE | 2 +- vendor/github.com/beevik/etree/README.md | 2 +- .../github.com/beevik/etree/RELEASE_NOTES.md | 85 ++ vendor/github.com/beevik/etree/etree.go | 1048 +++++++++++------ vendor/github.com/beevik/etree/helpers.go | 200 +++- vendor/github.com/beevik/etree/path.go | 203 ++-- vendor/github.com/crewjam/saml/.golangci.yml | 134 ++- vendor/github.com/crewjam/saml/CODEOWNERS | 1 + vendor/github.com/crewjam/saml/README.md | 4 +- .../crewjam/saml/identity_provider.go | 68 +- vendor/github.com/crewjam/saml/metadata.go | 49 + vendor/github.com/crewjam/saml/saml.go | 2 +- vendor/github.com/crewjam/saml/schema.go | 9 +- .../crewjam/saml/service_provider.go | 341 ++++-- vendor/github.com/crewjam/saml/xmlenc/cbc.go | 2 +- .../github.com/crewjam/saml/xmlenc/decrypt.go | 2 - .../github.com/crewjam/saml/xmlenc/digest.go | 2 +- vendor/github.com/crewjam/saml/xmlenc/fuzz.go | 1 + .../github.com/crewjam/saml/xmlenc/pubkey.go | 45 +- .../russellhaering/goxmldsig/canonicalize.go | 86 +- .../goxmldsig/etreeutils/sort.go | 25 +- .../russellhaering/goxmldsig/validate.go | 6 +- vendor/modules.txt | 10 +- 27 files changed, 1645 insertions(+), 712 deletions(-) delete mode 100644 vendor/github.com/beevik/etree/.travis.yml create mode 100644 vendor/github.com/crewjam/saml/CODEOWNERS diff --git a/go.mod b/go.mod index 93702e4b..7ae0c10d 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( git.autistici.org/id/go-sso v0.0.0-20241017184626-0e26b5e055dc git.autistici.org/id/keystore v0.0.0-20230901162242-63f23c4799e9 git.autistici.org/id/usermetadb v0.0.0-20241017171915-b5c24a0ff9b7 - github.com/crewjam/saml v0.4.14 + github.com/crewjam/saml v0.5.0 github.com/elazarl/go-bindata-assetfs v1.0.1 github.com/go-webauthn/webauthn v0.12.2 github.com/gorilla/csrf v1.7.2 @@ -29,7 +29,7 @@ require ( require ( github.com/NYTimes/gziphandler v1.1.1 // indirect - github.com/beevik/etree v1.1.0 // indirect + github.com/beevik/etree v1.5.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -52,7 +52,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/russellhaering/goxmldsig v1.3.0 // indirect + github.com/russellhaering/goxmldsig v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.9.0 // indirect diff --git a/go.sum b/go.sum index 8c98017d..3ba3476a 100644 --- a/go.sum +++ b/go.sum @@ -148,6 +148,8 @@ github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U github.com/bbrks/wrap/v2 v2.5.0/go.mod h1:FdEamYFrsjX8zlv3UXgnT3JxirrDv67jCDYaE0Q/qww= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs= +github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -216,6 +218,8 @@ github.com/crewjam/saml v0.4.13 h1:TYHggH/hwP7eArqiXSJUvtOPNzQDyQ7vwmwEqlFWhMc= github.com/crewjam/saml v0.4.13/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA= github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c= github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME= +github.com/crewjam/saml v0.5.0 h1:X6ZUB/qgD+qZERNL8YdicluZVueFQHO4auuhN8NZKCs= +github.com/crewjam/saml v0.5.0/go.mod h1:r0fDkmFe5URDgPrmtH0IYokva6fac3AUdstiPhyEolQ= github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -762,6 +766,8 @@ github.com/russellhaering/goxmldsig v1.2.0 h1:Y6GTTc9Un5hCxSzVz4UIWQ/zuVwDvzJk80 github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russellhaering/goxmldsig v1.3.0 h1:DllIWUgMy0cRUMfGiASiYEa35nsieyD3cigIwLonTPM= github.com/russellhaering/goxmldsig v1.3.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= +github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= +github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/vendor/github.com/beevik/etree/.travis.yml b/vendor/github.com/beevik/etree/.travis.yml deleted file mode 100644 index f4cb25d4..00000000 --- a/vendor/github.com/beevik/etree/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: go -sudo: false - -go: - - 1.11.x - - tip - -matrix: - allow_failures: - - go: tip - -script: - - go vet ./... - - go test -v ./... diff --git a/vendor/github.com/beevik/etree/CONTRIBUTORS b/vendor/github.com/beevik/etree/CONTRIBUTORS index 03211a85..24ab2dfb 100644 --- a/vendor/github.com/beevik/etree/CONTRIBUTORS +++ b/vendor/github.com/beevik/etree/CONTRIBUTORS @@ -8,3 +8,7 @@ Nicolas Piganeau (npiganeau) Chris Brown (ccbrown) Earncef Sequeira (earncef) Gabriel de Labachelerie (wuzuf) +Martin Dosch (mdosch) +Hugo Wetterberg (hugowetterberg) +Tobias Theel (nerzal) +Daniel Potapov (dpotapov) diff --git a/vendor/github.com/beevik/etree/LICENSE b/vendor/github.com/beevik/etree/LICENSE index 26f1f775..4ebd8567 100644 --- a/vendor/github.com/beevik/etree/LICENSE +++ b/vendor/github.com/beevik/etree/LICENSE @@ -1,4 +1,4 @@ -Copyright 2015-2019 Brett Vickers. All rights reserved. +Copyright 2015-2024 Brett Vickers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions diff --git a/vendor/github.com/beevik/etree/README.md b/vendor/github.com/beevik/etree/README.md index 08ec26b0..98a45183 100644 --- a/vendor/github.com/beevik/etree/README.md +++ b/vendor/github.com/beevik/etree/README.md @@ -1,5 +1,5 @@ -[](https://travis-ci.org/beevik/etree) [](https://godoc.org/github.com/beevik/etree) +[](https://github.com/beevik/etree/actions/workflows/go.yml) etree ===== diff --git a/vendor/github.com/beevik/etree/RELEASE_NOTES.md b/vendor/github.com/beevik/etree/RELEASE_NOTES.md index ee59d7ab..14ade682 100644 --- a/vendor/github.com/beevik/etree/RELEASE_NOTES.md +++ b/vendor/github.com/beevik/etree/RELEASE_NOTES.md @@ -1,3 +1,88 @@ +Release 1.5.0 +============= + +**Changes** + +* Added `Element` function `CreateChild`, which calls a continuation function + after creating and adding a child element. + +**Fixes** + +* Removed a potential conflict between two `ReadSettings` values. When + `AttrSingleQuote` is true, `CanonicalAttrVal` is forced to be false. + +Release 1.4.1 +============= + +**Changes** + +* Minimal go version updated to 1.21. +* Default-initialized CharsetReader causes same result as NewDocument(). +* When reading an XML document, attributes are parsed more efficiently. + +Release v1.4.0 +============== + +**New Features** + +* Add `AutoClose` option to `ReadSettings`. +* Add `ValidateInput` to `ReadSettings`. +* Add `NotNil` function to `Element`. +* Add `NextSibling` and `PrevSibling` functions to `Element`. + +Release v1.3.0 +============== + +**New Features** + +* Add support for double-quotes in filter path queries. +* Add `PreserveDuplicateAttrs` to `ReadSettings`. +* Add `ReindexChildren` to `Element`. + +Release v1.2.0 +============== + +**New Features** + +* Add the ability to write XML fragments using Token WriteTo functions. +* Add the ability to re-indent an XML element as though it were the root of + the document. +* Add a ReadSettings option to preserve CDATA blocks when reading and XML + document. + +Release v1.1.4 +============== + +**New Features** + +* Add the ability to preserve whitespace in leaf elements during indent. +* Add the ability to suppress a document-trailing newline during indent. +* Add choice of XML attribute quoting style (single-quote or double-quote). + +**Removed Features** + +* Removed the CDATA preservation change introduced in v1.1.3. It was + implemented in a way that broke the ability to process XML documents + encoded using non-UTF8 character sets. + +Release v1.1.3 +============== + +* XML reads now preserve CDATA sections instead of converting them to + standard character data. + +Release v1.1.2 +============== + +* Fixed a path parsing bug. +* The `Element.Text` function now handles comments embedded between + character data spans. + +Release v1.1.1 +============== + +* Updated go version in `go.mod` to 1.20 + Release v1.1.0 ============== diff --git a/vendor/github.com/beevik/etree/etree.go b/vendor/github.com/beevik/etree/etree.go index 9e24f901..e1dde1c3 100644 --- a/vendor/github.com/beevik/etree/etree.go +++ b/vendor/github.com/beevik/etree/etree.go @@ -13,44 +13,94 @@ import ( "errors" "io" "os" - "sort" + "slices" "strings" ) const ( - // NoIndent is used with Indent to disable all indenting. + // NoIndent is used with the IndentSettings record to remove all + // indenting. NoIndent = -1 ) // ErrXML is returned when XML parsing fails due to incorrect formatting. var ErrXML = errors.New("etree: invalid XML format") -// ReadSettings allow for changing the default behavior of the ReadFrom* -// methods. +// cdataPrefix is used to detect CDATA text when ReadSettings.PreserveCData is +// true. +var cdataPrefix = []byte("<![CDATA[") + +// ReadSettings determine the default behavior of the Document's ReadFrom* +// functions. type ReadSettings struct { - // CharsetReader to be passed to standard xml.Decoder. Default: nil. + // CharsetReader, if non-nil, defines a function to generate + // charset-conversion readers, converting from the provided non-UTF-8 + // charset into UTF-8. If nil, the ReadFrom* functions will use a + // "pass-through" CharsetReader that performs no conversion on the reader's + // data regardless of the value of the "charset" encoding string. Default: + // nil. CharsetReader func(charset string, input io.Reader) (io.Reader, error) // Permissive allows input containing common mistakes such as missing tags // or attribute values. Default: false. Permissive bool + // Preserve CDATA character data blocks when decoding XML (instead of + // converting it to normal character text). This entails additional + // processing and memory usage during ReadFrom* operations. Default: + // false. + PreserveCData bool + + // When an element has two or more attributes with the same name, + // preserve them instead of keeping only one. Default: false. + PreserveDuplicateAttrs bool + + // ValidateInput forces all ReadFrom* functions to validate that the + // provided input is composed of "well-formed"(*) XML before processing it. + // If invalid XML is detected, the ReadFrom* functions return an error. + // Because this option requires the input to be processed twice, it incurs a + // significant performance penalty. Default: false. + // + // (*) Note that this definition of "well-formed" is in the context of the + // go standard library's encoding/xml package. Go's encoding/xml package + // does not, in fact, guarantee well-formed XML as specified by the W3C XML + // recommendation. See: https://github.com/golang/go/issues/68299 + ValidateInput bool + // Entity to be passed to standard xml.Decoder. Default: nil. Entity map[string]string + + // When Permissive is true, AutoClose indicates a set of elements to + // consider closed immediately after they are opened, regardless of + // whether an end element is present. Commonly set to xml.HTMLAutoClose. + // Default: nil. + AutoClose []string +} + +// defaultCharsetReader is used by the xml decoder when the ReadSettings +// CharsetReader value is nil. It behaves as a "pass-through", ignoring +// the requested charset parameter and skipping conversion altogether. +func defaultCharsetReader(charset string, input io.Reader) (io.Reader, error) { + return input, nil } -// newReadSettings creates a default ReadSettings record. -func newReadSettings() ReadSettings { +// dup creates a duplicate of the ReadSettings object. +func (s *ReadSettings) dup() ReadSettings { + var entityCopy map[string]string + if s.Entity != nil { + entityCopy = make(map[string]string) + for k, v := range s.Entity { + entityCopy[k] = v + } + } return ReadSettings{ - CharsetReader: func(label string, input io.Reader) (io.Reader, error) { - return input, nil - }, - Permissive: false, + CharsetReader: s.CharsetReader, + Permissive: s.Permissive, + Entity: entityCopy, } } -// WriteSettings allow for changing the serialization behavior of the WriteTo* -// methods. +// WriteSettings determine the behavior of the Document's WriteTo* functions. type WriteSettings struct { // CanonicalEndTags forces the production of XML end tags, even for // elements that have no child elements. Default: false. @@ -63,40 +113,116 @@ type WriteSettings struct { // CanonicalAttrVal forces the production of XML character references for // attribute value characters &, < and ". If false, XML character - // references are also produced for > and '. Default: false. + // references are also produced for > and '. Ignored when AttrSingleQuote + // is true. Default: false. CanonicalAttrVal bool - // When outputting indented XML, use a carriage return and linefeed - // ("\r\n") as a new-line delimiter instead of just a linefeed ("\n"). - // This is useful on Windows-based systems. + // AttrSingleQuote causes attributes to use single quotes (attr='example') + // instead of double quotes (attr = "example") when set to true. Default: + // false. + AttrSingleQuote bool + + // UseCRLF causes the document's Indent* functions to use a carriage return + // followed by a linefeed ("\r\n") when outputting a newline. If false, + // only a linefeed is used ("\n"). Default: false. + // + // Deprecated: UseCRLF is deprecated. Use IndentSettings.UseCRLF instead. UseCRLF bool } -// newWriteSettings creates a default WriteSettings record. -func newWriteSettings() WriteSettings { - return WriteSettings{ - CanonicalEndTags: false, - CanonicalText: false, - CanonicalAttrVal: false, - UseCRLF: false, +// dup creates a duplicate of the WriteSettings object. +func (s *WriteSettings) dup() WriteSettings { + return *s +} + +// IndentSettings determine the behavior of the Document's Indent* functions. +type IndentSettings struct { + // Spaces indicates the number of spaces to insert for each level of + // indentation. Set to etree.NoIndent to remove all indentation. Ignored + // when UseTabs is true. Default: 4. + Spaces int + + // UseTabs causes tabs to be used instead of spaces when indenting. + // Default: false. + UseTabs bool + + // UseCRLF causes newlines to be written as a carriage return followed by + // a linefeed ("\r\n"). If false, only a linefeed character is output + // for a newline ("\n"). Default: false. + UseCRLF bool + + // PreserveLeafWhitespace causes indent functions to preserve whitespace + // within XML elements containing only non-CDATA character data. Default: + // false. + PreserveLeafWhitespace bool + + // SuppressTrailingWhitespace suppresses the generation of a trailing + // whitespace characters (such as newlines) at the end of the indented + // document. Default: false. + SuppressTrailingWhitespace bool +} + +// NewIndentSettings creates a default IndentSettings record. +func NewIndentSettings() *IndentSettings { + return &IndentSettings{ + Spaces: 4, + UseTabs: false, + UseCRLF: false, + PreserveLeafWhitespace: false, + SuppressTrailingWhitespace: false, } } -// A Token is an empty interface that represents an Element, CharData, -// Comment, Directive, or ProcInst. +type indentFunc func(depth int) string + +func getIndentFunc(s *IndentSettings) indentFunc { + if s.UseTabs { + if s.UseCRLF { + return func(depth int) string { return indentCRLF(depth, indentTabs) } + } else { + return func(depth int) string { return indentLF(depth, indentTabs) } + } + } else { + if s.Spaces < 0 { + return func(depth int) string { return "" } + } else if s.UseCRLF { + return func(depth int) string { return indentCRLF(depth*s.Spaces, indentSpaces) } + } else { + return func(depth int) string { return indentLF(depth*s.Spaces, indentSpaces) } + } + } +} + +// Writer is the interface that wraps the Write* functions called by each token +// type's WriteTo function. +type Writer interface { + io.StringWriter + io.ByteWriter + io.Writer +} + +// A Token is an interface type used to represent XML elements, character +// data, CDATA sections, XML comments, XML directives, and XML processing +// instructions. type Token interface { Parent() *Element Index() int + WriteTo(w Writer, s *WriteSettings) dup(parent *Element) Token setParent(parent *Element) setIndex(index int) - writeTo(w *bufio.Writer, s *WriteSettings) } -// A Document is a container holding a complete XML hierarchy. Its embedded -// element contains zero or more children, one of which is usually the root -// element. The embedded element may include other children such as -// processing instructions or BOM CharData tokens. +// A Document is a container holding a complete XML tree. +// +// A document has a single embedded element, which contains zero or more child +// tokens, one of which is usually the root element. The embedded element may +// include other children such as processing instruction tokens or character +// data tokens. The document's embedded element is never directly serialized; +// only its children are. +// +// A document also contains read and write settings, which influence the way +// the document is deserialized, serialized, and indented. type Document struct { Element ReadSettings ReadSettings @@ -112,7 +238,7 @@ type Element struct { index int // token index in parent's children } -// An Attr represents a key-value attribute of an XML element. +// An Attr represents a key-value attribute within an XML element. type Attr struct { Space, Key string // The attribute's namespace prefix and key Value string // The attribute value string @@ -123,17 +249,18 @@ type Attr struct { type charDataFlags uint8 const ( - // The CharData was created by an indent function as whitespace. + // The CharData contains only whitespace. whitespaceFlag charDataFlags = 1 << iota // The CharData contains a CDATA section. cdataFlag ) -// CharData can be used to represent character data or a CDATA section within -// an XML document. +// CharData may be used to represent simple text data or a CDATA section +// within an XML document. The Data property should never be modified +// directly; use the SetData function instead. type CharData struct { - Data string + Data string // the simple text or CDATA section content parent *Element index int flags charDataFlags @@ -141,22 +268,22 @@ type CharData struct { // A Comment represents an XML comment. type Comment struct { - Data string + Data string // the comment's text parent *Element index int } // A Directive represents an XML directive. type Directive struct { - Data string + Data string // the directive string parent *Element index int } // A ProcInst represents an XML processing instruction. type ProcInst struct { - Target string - Inst string + Target string // the processing instruction target + Inst string // the processing instruction value parent *Element index int } @@ -164,19 +291,30 @@ type ProcInst struct { // NewDocument creates an XML document without a root element. func NewDocument() *Document { return &Document{ - Element{Child: make([]Token, 0)}, - newReadSettings(), - newWriteSettings(), + Element: Element{Child: make([]Token, 0)}, } } +// NewDocumentWithRoot creates an XML document and sets the element 'e' as its +// root element. If the element 'e' is already part of another document, it is +// first removed from its existing document. +func NewDocumentWithRoot(e *Element) *Document { + d := NewDocument() + d.SetRoot(e) + return d +} + // Copy returns a recursive, deep copy of the document. func (d *Document) Copy() *Document { - return &Document{*(d.dup(nil).(*Element)), d.ReadSettings, d.WriteSettings} + return &Document{ + Element: *(d.Element.dup(nil).(*Element)), + ReadSettings: d.ReadSettings.dup(), + WriteSettings: d.WriteSettings.dup(), + } } -// Root returns the root element of the document, or nil if there is no root -// element. +// Root returns the root element of the document. It returns nil if there is +// no root element. func (d *Document) Root() *Element { for _, t := range d.Child { if c, ok := t.(*Element); ok { @@ -186,25 +324,23 @@ func (d *Document) Root() *Element { return nil } -// SetRoot replaces the document's root element with e. If the document -// already has a root when this function is called, then the document's -// original root is unbound first. If the element e is bound to another -// document (or to another element within a document), then it is unbound -// first. +// SetRoot replaces the document's root element with the element 'e'. If the +// document already has a root element when this function is called, then the +// existing root element is unbound from the document. If the element 'e' is +// part of another document, then it is unbound from the other document. func (d *Document) SetRoot(e *Element) { if e.parent != nil { e.parent.RemoveChild(e) } - p := &d.Element - e.setParent(p) - // If there is already a root element, replace it. + p := &d.Element for i, t := range p.Child { if _, ok := t.(*Element); ok { t.setParent(nil) t.setIndex(-1) p.Child[i] = e + e.setParent(p) e.setIndex(i) return } @@ -214,51 +350,104 @@ func (d *Document) SetRoot(e *Element) { p.addChild(e) } -// ReadFrom reads XML from the reader r into the document d. It returns the -// number of bytes read and any error encountered. +// ReadFrom reads XML from the reader 'r' into this document. The function +// returns the number of bytes read and any error encountered. func (d *Document) ReadFrom(r io.Reader) (n int64, err error) { + if d.ReadSettings.ValidateInput { + b, err := io.ReadAll(r) + if err != nil { + return 0, err + } + if err := validateXML(bytes.NewReader(b), d.ReadSettings); err != nil { + return 0, err + } + r = bytes.NewReader(b) + } return d.Element.readFrom(r, d.ReadSettings) } -// ReadFromFile reads XML from the string s into the document d. -func (d *Document) ReadFromFile(filename string) error { - f, err := os.Open(filename) +// ReadFromFile reads XML from a local file at path 'filepath' into this +// document. +func (d *Document) ReadFromFile(filepath string) error { + f, err := os.Open(filepath) if err != nil { return err } defer f.Close() + _, err = d.ReadFrom(f) return err } -// ReadFromBytes reads XML from the byte slice b into the document d. +// ReadFromBytes reads XML from the byte slice 'b' into the this document. func (d *Document) ReadFromBytes(b []byte) error { - _, err := d.ReadFrom(bytes.NewReader(b)) + if d.ReadSettings.ValidateInput { + if err := validateXML(bytes.NewReader(b), d.ReadSettings); err != nil { + return err + } + } + _, err := d.Element.readFrom(bytes.NewReader(b), d.ReadSettings) return err } -// ReadFromString reads XML from the string s into the document d. +// ReadFromString reads XML from the string 's' into this document. func (d *Document) ReadFromString(s string) error { - _, err := d.ReadFrom(strings.NewReader(s)) + if d.ReadSettings.ValidateInput { + if err := validateXML(strings.NewReader(s), d.ReadSettings); err != nil { + return err + } + } + _, err := d.Element.readFrom(strings.NewReader(s), d.ReadSettings) return err } -// WriteTo serializes an XML document into the writer w. It -// returns the number of bytes written and any error encountered. +// validateXML determines if the data read from the reader 'r' contains +// well-formed XML according to the rules set by the go xml package. +func validateXML(r io.Reader, settings ReadSettings) error { + dec := newDecoder(r, settings) + err := dec.Decode(new(interface{})) + if err != nil { + return err + } + + // If there are any trailing tokens after unmarshalling with Decode(), + // then the XML input didn't terminate properly. + _, err = dec.Token() + if err == io.EOF { + return nil + } + return ErrXML +} + +// newDecoder creates an XML decoder for the reader 'r' configured using +// the provided read settings. +func newDecoder(r io.Reader, settings ReadSettings) *xml.Decoder { + d := xml.NewDecoder(r) + d.CharsetReader = settings.CharsetReader + if d.CharsetReader == nil { + d.CharsetReader = defaultCharsetReader + } + d.Strict = !settings.Permissive + d.Entity = settings.Entity + d.AutoClose = settings.AutoClose + return d +} + +// WriteTo serializes the document out to the writer 'w'. The function returns +// the number of bytes written and any error encountered. func (d *Document) WriteTo(w io.Writer) (n int64, err error) { - cw := newCountWriter(w) - b := bufio.NewWriter(cw) + xw := newXmlWriter(w) + b := bufio.NewWriter(xw) for _, c := range d.Child { - c.writeTo(b, &d.WriteSettings) + c.WriteTo(b, &d.WriteSettings) } - err, n = b.Flush(), cw.bytes + err, n = b.Flush(), xw.bytes return } -// WriteToFile serializes an XML document into the file named -// filename. -func (d *Document) WriteToFile(filename string) error { - f, err := os.Create(filename) +// WriteToFile serializes the document out to the file at path 'filepath'. +func (d *Document) WriteToFile(filepath string) error { + f, err := os.Create(filepath) if err != nil { return err } @@ -267,8 +456,7 @@ func (d *Document) WriteToFile(filename string) error { return err } -// WriteToBytes serializes the XML document into a slice of -// bytes. +// WriteToBytes serializes this document into a slice of bytes. func (d *Document) WriteToBytes() (b []byte, err error) { var buf bytes.Buffer if _, err = d.WriteTo(&buf); err != nil { @@ -277,7 +465,7 @@ func (d *Document) WriteToBytes() (b []byte, err error) { return buf.Bytes(), nil } -// WriteToString serializes the XML document into a string. +// WriteToString serializes this document into a string. func (d *Document) WriteToString() (s string, err error) { var b []byte if b, err = d.WriteToBytes(); err != nil { @@ -286,41 +474,54 @@ func (d *Document) WriteToString() (s string, err error) { return string(b), nil } -type indentFunc func(depth int) string - // Indent modifies the document's element tree by inserting character data -// tokens containing newlines and indentation. The amount of indentation per -// depth level is given as spaces. Pass etree.NoIndent for spaces if you want -// no indentation at all. +// tokens containing newlines and spaces for indentation. The amount of +// indentation per depth level is given by the 'spaces' parameter. Other than +// the number of spaces, default IndentSettings are used. func (d *Document) Indent(spaces int) { - var indent indentFunc - switch { - case spaces < 0: - indent = func(depth int) string { return "" } - case d.WriteSettings.UseCRLF == true: - indent = func(depth int) string { return indentCRLF(depth*spaces, indentSpaces) } - default: - indent = func(depth int) string { return indentLF(depth*spaces, indentSpaces) } - } - d.Element.indent(0, indent) + s := NewIndentSettings() + s.Spaces = spaces + d.IndentWithSettings(s) } // IndentTabs modifies the document's element tree by inserting CharData -// tokens containing newlines and tabs for indentation. One tab is used per -// indentation level. +// tokens containing newlines and tabs for indentation. One tab is used per +// indentation level. Other than the use of tabs, default IndentSettings +// are used. func (d *Document) IndentTabs() { - var indent indentFunc - switch d.WriteSettings.UseCRLF { - case true: - indent = func(depth int) string { return indentCRLF(depth, indentTabs) } - default: - indent = func(depth int) string { return indentLF(depth, indentTabs) } + s := NewIndentSettings() + s.UseTabs = true + d.IndentWithSettings(s) +} + +// IndentWithSettings modifies the document's element tree by inserting +// character data tokens containing newlines and indentation. The behavior +// of the indentation algorithm is configured by the indent settings. +func (d *Document) IndentWithSettings(s *IndentSettings) { + // WriteSettings.UseCRLF is deprecated. Until removed from the package, it + // overrides IndentSettings.UseCRLF when true. + if d.WriteSettings.UseCRLF { + s.UseCRLF = true + } + + d.Element.indent(0, getIndentFunc(s), s) + + if s.SuppressTrailingWhitespace { + d.Element.stripTrailingWhitespace() } - d.Element.indent(0, indent) } -// NewElement creates an unparented element with the specified tag. The tag -// may be prefixed by a namespace prefix and a colon. +// Unindent modifies the document's element tree by removing character data +// tokens containing only whitespace. Other than the removal of indentation, +// default IndentSettings are used. +func (d *Document) Unindent() { + s := NewIndentSettings() + s.Spaces = NoIndent + d.IndentWithSettings(s) +} + +// NewElement creates an unparented element with the specified tag (i.e., +// name). The tag may include a namespace prefix followed by a colon. func NewElement(tag string) *Element { space, stag := spaceDecompose(tag) return newElement(space, stag, nil) @@ -345,7 +546,8 @@ func newElement(space, tag string, parent *Element) *Element { // Copy creates a recursive, deep copy of the element and all its attributes // and children. The returned element has no parent but can be parented to a -// another element using AddElement, or to a document using SetRoot. +// another element using AddChild, or added to a document with SetRoot or +// NewDocumentWithRoot. func (e *Element) Copy() *Element { return e.dup(nil).(*Element) } @@ -400,16 +602,6 @@ func (e *Element) findDefaultNamespaceURI() string { return e.parent.findDefaultNamespaceURI() } -// hasText returns true if the element has character data immediately -// folllowing the element's opening tag. -func (e *Element) hasText() bool { - if len(e.Child) == 0 { - return false - } - _, ok := e.Child[0].(*CharData) - return ok -} - // namespacePrefix returns the namespace prefix associated with the element. func (e *Element) namespacePrefix() string { return e.Space @@ -420,6 +612,15 @@ func (e *Element) name() string { return e.Tag } +// ReindexChildren recalculates the index values of the element's child +// tokens. This is necessary only if you have manually manipulated the +// element's `Child` array. +func (e *Element) ReindexChildren() { + for i := 0; i < len(e.Child); i++ { + e.Child[i].setIndex(i) + } +} + // Text returns all character data immediately following the element's opening // tag. func (e *Element) Text() string { @@ -433,8 +634,10 @@ func (e *Element) Text() string { if text == "" { text = cd.Data } else { - text = text + cd.Data + text += cd.Data } + } else if _, ok := ch.(*Comment); ok { + // ignore } else { break } @@ -470,7 +673,7 @@ func (e *Element) Tail() string { if text == "" { text = cd.Data } else { - text = text + cd.Data + text += cd.Data } } else { break @@ -548,30 +751,51 @@ func (e *Element) findTermCharDataIndex(start int) int { return len(e.Child) } -// CreateElement creates an element with the specified tag and adds it as the -// last child element of the element e. The tag may be prefixed by a namespace -// prefix and a colon. +// CreateElement creates a new element with the specified tag (i.e., name) and +// adds it as the last child of element 'e'. The tag may include a prefix +// followed by a colon. func (e *Element) CreateElement(tag string) *Element { space, stag := spaceDecompose(tag) return newElement(space, stag, e) } -// AddChild adds the token t as the last child of element e. If token t was -// already the child of another element, it is first removed from its current +// CreateChild performs the same task as CreateElement but calls a +// continuation function after the child element is created, allowing +// additional actions to be performed on the child element before returning. +// +// This method of element creation is particularly useful when building nested +// XML documents from code. For example: +// +// org := doc.CreateChild("organization", func(e *Element) { +// e.CreateComment("Mary") +// e.CreateChild("person", func(e *Element) { +// e.CreateAttr("name", "Mary") +// e.CreateAttr("age", "30") +// e.CreateAttr("hair", "brown") +// }) +// }) +func (e *Element) CreateChild(tag string, cont func(e *Element)) *Element { + child := e.CreateElement(tag) + cont(child) + return child +} + +// AddChild adds the token 't' as the last child of the element. If token 't' +// was already the child of another element, it is first removed from its // parent element. func (e *Element) AddChild(t Token) { if t.Parent() != nil { t.Parent().RemoveChild(t) } - - t.setParent(e) e.addChild(t) } -// InsertChild inserts the token t before e's existing child token ex. If ex -// is nil or ex is not a child of e, then t is added to the end of e's child -// token list. If token t was already the child of another element, it is -// first removed from its current parent element. +// InsertChild inserts the token 't' into this element's list of children just +// before the element's existing child token 'ex'. If the existing element +// 'ex' does not appear in this element's list of child tokens, then 't' is +// added to the end of this element's list of child tokens. If token 't' is +// already the child of another element, it is first removed from the other +// element's list of child tokens. // // Deprecated: InsertChild is deprecated. Use InsertChildAt instead. func (e *Element) InsertChild(ex Token, t Token) { @@ -596,10 +820,10 @@ func (e *Element) InsertChild(ex Token, t Token) { } } -// InsertChildAt inserts the token t into the element e's list of child tokens -// just before the requested index. If the index is greater than or equal to -// the length of the list of child tokens, the token t is added to the end of -// the list. +// InsertChildAt inserts the token 't' into this element's list of child +// tokens just before the requested 'index'. If the index is greater than or +// equal to the length of the list of child tokens, then the token 't' is +// added to the end of the list of child tokens. func (e *Element) InsertChildAt(index int, t Token) { if index >= len(e.Child) { e.AddChild(t) @@ -624,9 +848,9 @@ func (e *Element) InsertChildAt(index int, t Token) { } } -// RemoveChild attempts to remove the token t from element e's list of -// children. If the token t is a child of e, then it is returned. Otherwise, -// nil is returned. +// RemoveChild attempts to remove the token 't' from this element's list of +// child tokens. If the token 't' was a child of this element, then it is +// removed and returned. Otherwise, nil is returned. func (e *Element) RemoveChild(t Token) Token { if t.Parent() != e { return nil @@ -634,9 +858,9 @@ func (e *Element) RemoveChild(t Token) Token { return e.RemoveChildAt(t.Index()) } -// RemoveChildAt removes the index-th child token from the element e. The -// removed child token is returned. If the index is out of bounds, no child is -// removed and nil is returned. +// RemoveChildAt removes the child token appearing in slot 'index' of this +// element's list of child tokens. The removed child token is then returned. +// If the index is out of bounds, no child is removed and nil is returned. func (e *Element) RemoveChildAt(index int) Token { if index >= len(e.Child) { return nil @@ -652,43 +876,106 @@ func (e *Element) RemoveChildAt(index int) Token { return t } -// ReadFrom reads XML from the reader r and stores the result as a new child -// of element e. +// autoClose analyzes the stack's top element and the current token to decide +// whether the top element should be closed. +func (e *Element) autoClose(stack *stack[*Element], t xml.Token, tags []string) { + if stack.empty() { + return + } + + top := stack.peek() + + for _, tag := range tags { + if strings.EqualFold(tag, top.FullTag()) { + if e, ok := t.(xml.EndElement); !ok || + !strings.EqualFold(e.Name.Space, top.Space) || + !strings.EqualFold(e.Name.Local, top.Tag) { + stack.pop() + } + break + } + } +} + +// ReadFrom reads XML from the reader 'ri' and stores the result as a new +// child of this element. func (e *Element) readFrom(ri io.Reader, settings ReadSettings) (n int64, err error) { - r := newCountReader(ri) - dec := xml.NewDecoder(r) - dec.CharsetReader = settings.CharsetReader - dec.Strict = !settings.Permissive - dec.Entity = settings.Entity - var stack stack + var r xmlReader + var pr *xmlPeekReader + if settings.PreserveCData { + pr = newXmlPeekReader(ri) + r = pr + } else { + r = newXmlSimpleReader(ri) + } + + attrCheck := make(map[xml.Name]int) + dec := newDecoder(r, settings) + + var stack stack[*Element] stack.push(e) for { + if pr != nil { + pr.PeekPrepare(dec.InputOffset(), len(cdataPrefix)) + } + t, err := dec.RawToken() + + if settings.Permissive && settings.AutoClose != nil { + e.autoClose(&stack, t, settings.AutoClose) + } + switch { case err == io.EOF: - return r.bytes, nil + if len(stack.data) != 1 { + return r.Bytes(), ErrXML + } + return r.Bytes(), nil case err != nil: - return r.bytes, err + return r.Bytes(), err case stack.empty(): - return r.bytes, ErrXML + return r.Bytes(), ErrXML } - top := stack.peek().(*Element) + top := stack.peek() switch t := t.(type) { case xml.StartElement: e := newElement(t.Name.Space, t.Name.Local, top) - for _, a := range t.Attr { - e.createAttr(a.Name.Space, a.Name.Local, a.Value, e) + if settings.PreserveDuplicateAttrs || len(t.Attr) < 2 { + for _, a := range t.Attr { + e.addAttr(a.Name.Space, a.Name.Local, a.Value) + } + } else { + for _, a := range t.Attr { + if i, contains := attrCheck[a.Name]; contains { + e.Attr[i].Value = a.Value + } else { + attrCheck[a.Name] = e.addAttr(a.Name.Space, a.Name.Local, a.Value) + } + } + clear(attrCheck) } stack.push(e) case xml.EndElement: + if top.Tag != t.Name.Local || top.Space != t.Name.Space { + return r.Bytes(), ErrXML + } stack.pop() case xml.CharData: data := string(t) var flags charDataFlags - if isWhitespace(data) { - flags = whitespaceFlag + if pr != nil { + peekBuf := pr.PeekFinalize() + if bytes.Equal(peekBuf, cdataPrefix) { + flags = cdataFlag + } else if isWhitespace(data) { + flags = whitespaceFlag + } + } else { + if isWhitespace(data) { + flags = whitespaceFlag + } } newCharData(data, flags, top) case xml.Comment: @@ -701,9 +988,10 @@ func (e *Element) readFrom(ri io.Reader, settings ReadSettings) (n int64, err er } } -// SelectAttr finds an element attribute matching the requested key and -// returns it if found. Returns nil if no matching attribute is found. The key -// may be prefixed by a namespace prefix and a colon. +// SelectAttr finds an element attribute matching the requested 'key' and, if +// found, returns a pointer to the matching attribute. The function returns +// nil if no matching attribute is found. The key may include a namespace +// prefix followed by a colon. func (e *Element) SelectAttr(key string) *Attr { space, skey := spaceDecompose(key) for i, a := range e.Attr { @@ -714,9 +1002,10 @@ func (e *Element) SelectAttr(key string) *Attr { return nil } -// SelectAttrValue finds an element attribute matching the requested key and -// returns its value if found. The key may be prefixed by a namespace prefix -// and a colon. If the key is not found, the dflt value is returned instead. +// SelectAttrValue finds an element attribute matching the requested 'key' and +// returns its value if found. If no matching attribute is found, the function +// returns the 'dflt' value instead. The key may include a namespace prefix +// followed by a colon. func (e *Element) SelectAttrValue(key, dflt string) string { space, skey := spaceDecompose(key) for _, a := range e.Attr { @@ -727,7 +1016,7 @@ func (e *Element) SelectAttrValue(key, dflt string) string { return dflt } -// ChildElements returns all elements that are children of element e. +// ChildElements returns all elements that are children of this element. func (e *Element) ChildElements() []*Element { var elements []*Element for _, t := range e.Child { @@ -738,9 +1027,9 @@ func (e *Element) ChildElements() []*Element { return elements } -// SelectElement returns the first child element with the given tag. The tag -// may be prefixed by a namespace prefix and a colon. Returns nil if no -// element with a matching tag was found. +// SelectElement returns the first child element with the given 'tag' (i.e., +// name). The function returns nil if no child element matching the tag is +// found. The tag may include a namespace prefix followed by a colon. func (e *Element) SelectElement(tag string) *Element { space, stag := spaceDecompose(tag) for _, t := range e.Child { @@ -751,8 +1040,8 @@ func (e *Element) SelectElement(tag string) *Element { return nil } -// SelectElements returns a slice of all child elements with the given tag. -// The tag may be prefixed by a namespace prefix and a colon. +// SelectElements returns a slice of all child elements with the given 'tag' +// (i.e., name). The tag may include a namespace prefix followed by a colon. func (e *Element) SelectElements(tag string) []*Element { space, stag := spaceDecompose(tag) var elements []*Element @@ -764,39 +1053,58 @@ func (e *Element) SelectElements(tag string) []*Element { return elements } -// FindElement returns the first element matched by the XPath-like path -// string. Returns nil if no element is found using the path. Panics if an -// invalid path string is supplied. +// FindElement returns the first element matched by the XPath-like 'path' +// string. The function returns nil if no child element is found using the +// path. It panics if an invalid path string is supplied. func (e *Element) FindElement(path string) *Element { return e.FindElementPath(MustCompilePath(path)) } -// FindElementPath returns the first element matched by the XPath-like path -// string. Returns nil if no element is found using the path. +// FindElementPath returns the first element matched by the 'path' object. The +// function returns nil if no element is found using the path. func (e *Element) FindElementPath(path Path) *Element { p := newPather() elements := p.traverse(e, path) - switch { - case len(elements) > 0: + if len(elements) > 0 { return elements[0] - default: - return nil } + return nil } -// FindElements returns a slice of elements matched by the XPath-like path -// string. Panics if an invalid path string is supplied. +// FindElements returns a slice of elements matched by the XPath-like 'path' +// string. The function returns nil if no child element is found using the +// path. It panics if an invalid path string is supplied. func (e *Element) FindElements(path string) []*Element { return e.FindElementsPath(MustCompilePath(path)) } -// FindElementsPath returns a slice of elements matched by the Path object. +// FindElementsPath returns a slice of elements matched by the 'path' object. func (e *Element) FindElementsPath(path Path) []*Element { p := newPather() return p.traverse(e, path) } -// GetPath returns the absolute path of the element. +// NotNil returns the receiver element if it isn't nil; otherwise, it returns +// an unparented element with an empty string tag. This function simplifies +// the task of writing code to ignore not-found results from element queries. +// For example, instead of writing this: +// +// if e := doc.SelectElement("enabled"); e != nil { +// e.SetText("true") +// } +// +// You could write this: +// +// doc.SelectElement("enabled").NotNil().SetText("true") +func (e *Element) NotNil() *Element { + if e == nil { + return NewElement("") + } + return e +} + +// GetPath returns the absolute path of the element. The absolute path is the +// full path from the document's root. func (e *Element) GetPath() string { path := []string{} for seg := e; seg != nil; seg = seg.Parent() { @@ -813,9 +1121,9 @@ func (e *Element) GetPath() string { return "/" + strings.Join(path, "/") } -// GetRelativePath returns the path of the element relative to the source +// GetRelativePath returns the path of this element relative to the 'source' // element. If the two elements are not part of the same element tree, then -// GetRelativePath returns the empty string. +// the function returns the empty string. func (e *Element) GetRelativePath(source *Element) string { var path []*Element @@ -884,10 +1192,20 @@ func (e *Element) GetRelativePath(source *Element) string { return strings.Join(parts, "/") } -// indent recursively inserts proper indentation between an -// XML element's child tokens. -func (e *Element) indent(depth int, indent indentFunc) { - e.stripIndent() +// IndentWithSettings modifies the element and its child tree by inserting +// character data tokens containing newlines and indentation. The behavior of +// the indentation algorithm is configured by the indent settings. Because +// this function indents the element as if it were at the root of a document, +// it is most useful when called just before writing the element as an XML +// fragment using WriteTo. +func (e *Element) IndentWithSettings(s *IndentSettings) { + e.indent(1, getIndentFunc(s), s) +} + +// indent recursively inserts proper indentation between an XML element's +// child tokens. +func (e *Element) indent(depth int, indent indentFunc, s *IndentSettings) { + e.stripIndent(s) n := len(e.Child) if n == 0 { return @@ -915,7 +1233,7 @@ func (e *Element) indent(depth int, indent indentFunc) { // Recursively process child elements. if ce, ok := c.(*Element); ok { - ce.indent(depth+1, indent) + ce.indent(depth+1, indent, s) } } @@ -931,7 +1249,7 @@ func (e *Element) indent(depth int, indent indentFunc) { } // stripIndent removes any previously inserted indentation. -func (e *Element) stripIndent() { +func (e *Element) stripIndent(s *IndentSettings) { // Count the number of non-indent child tokens n := len(e.Child) for _, c := range e.Child { @@ -942,6 +1260,9 @@ func (e *Element) stripIndent() { if n == len(e.Child) { return } + if n == 0 && len(e.Child) == 1 && s.PreserveLeafWhitespace { + return + } // Strip out indent CharData newChild := make([]Token, n) @@ -957,6 +1278,17 @@ func (e *Element) stripIndent() { e.Child = newChild } +// stripTrailingWhitespace removes any trailing whitespace CharData tokens +// from the element's children. +func (e *Element) stripTrailingWhitespace() { + for i := len(e.Child) - 1; i >= 0; i-- { + if cd, ok := e.Child[i].(*CharData); !ok || !cd.IsWhitespace() { + e.Child = e.Child[:i+1] + return + } + } +} + // dup duplicates the element. func (e *Element) dup(parent *Element) Token { ne := &Element{ @@ -970,47 +1302,63 @@ func (e *Element) dup(parent *Element) Token { for i, t := range e.Child { ne.Child[i] = t.dup(ne) } - for i, a := range e.Attr { - ne.Attr[i] = a - } + copy(ne.Attr, e.Attr) return ne } -// Parent returns the element token's parent element, or nil if it has no -// parent. +// NextSibling returns this element's next sibling element. It returns nil if +// there is no next sibling element. +func (e *Element) NextSibling() *Element { + if e.parent == nil { + return nil + } + for i := e.index + 1; i < len(e.parent.Child); i++ { + if s, ok := e.parent.Child[i].(*Element); ok { + return s + } + } + return nil +} + +// PrevSibling returns this element's preceding sibling element. It returns +// nil if there is no preceding sibling element. +func (e *Element) PrevSibling() *Element { + if e.parent == nil { + return nil + } + for i := e.index - 1; i >= 0; i-- { + if s, ok := e.parent.Child[i].(*Element); ok { + return s + } + } + return nil +} + +// Parent returns this element's parent element. It returns nil if this +// element has no parent. func (e *Element) Parent() *Element { return e.parent } // Index returns the index of this element within its parent element's -// list of child tokens. If this element has no parent element, the index -// is -1. +// list of child tokens. If this element has no parent, then the function +// returns -1. func (e *Element) Index() int { return e.index } -// setParent replaces the element token's parent. -func (e *Element) setParent(parent *Element) { - e.parent = parent -} - -// setIndex sets the element token's index within its parent's Child slice. -func (e *Element) setIndex(index int) { - e.index = index -} - -// writeTo serializes the element to the writer w. -func (e *Element) writeTo(w *bufio.Writer, s *WriteSettings) { +// WriteTo serializes the element to the writer w. +func (e *Element) WriteTo(w Writer, s *WriteSettings) { w.WriteByte('<') w.WriteString(e.FullTag()) for _, a := range e.Attr { w.WriteByte(' ') - a.writeTo(w, s) + a.WriteTo(w, s) } if len(e.Child) > 0 { - w.WriteString(">") + w.WriteByte('>') for _, c := range e.Child { - c.writeTo(w, s) + c.WriteTo(w, s) } w.Write([]byte{'<', '/'}) w.WriteString(e.FullTag()) @@ -1026,42 +1374,58 @@ func (e *Element) writeTo(w *bufio.Writer, s *WriteSettings) { } } +// setParent replaces this element token's parent. +func (e *Element) setParent(parent *Element) { + e.parent = parent +} + +// setIndex sets this element token's index within its parent's Child slice. +func (e *Element) setIndex(index int) { + e.index = index +} + // addChild adds a child token to the element e. func (e *Element) addChild(t Token) { + t.setParent(e) t.setIndex(len(e.Child)) e.Child = append(e.Child, t) } -// CreateAttr creates an attribute and adds it to element e. The key may be -// prefixed by a namespace prefix and a colon. If an attribute with the key -// already exists, its value is replaced. +// CreateAttr creates an attribute with the specified 'key' and 'value' and +// adds it to this element. If an attribute with same key already exists on +// this element, then its value is replaced. The key may include a namespace +// prefix followed by a colon. func (e *Element) CreateAttr(key, value string) *Attr { space, skey := spaceDecompose(key) - return e.createAttr(space, skey, value, e) -} -// createAttr is a helper function that creates attributes. -func (e *Element) createAttr(space, key, value string, parent *Element) *Attr { for i, a := range e.Attr { - if space == a.Space && key == a.Key { + if space == a.Space && skey == a.Key { e.Attr[i].Value = value return &e.Attr[i] } } + + i := e.addAttr(space, skey, value) + return &e.Attr[i] +} + +// addAttr is a helper function that adds an attribute to an element. Returns +// the index of the added attribute. +func (e *Element) addAttr(space, key, value string) int { a := Attr{ Space: space, Key: key, Value: value, - element: parent, + element: e, } e.Attr = append(e.Attr, a) - return &e.Attr[len(e.Attr)-1] + return len(e.Attr) - 1 } -// RemoveAttr removes and returns a copy of the first attribute of the element -// whose key matches the given key. The key may be prefixed by a namespace -// prefix and a colon. If a matching attribute does not exist, nil is -// returned. +// RemoveAttr removes the first attribute of this element whose key matches +// 'key'. It returns a copy of the removed attribute if a match is found. If +// no match is found, it returns nil. The key may include a namespace prefix +// followed by a colon. func (e *Element) RemoveAttr(key string) *Attr { space, skey := spaceDecompose(key) for i, a := range e.Attr { @@ -1078,30 +1442,17 @@ func (e *Element) RemoveAttr(key string) *Attr { return nil } -// SortAttrs sorts the element's attributes lexicographically by key. +// SortAttrs sorts this element's attributes lexicographically by key. func (e *Element) SortAttrs() { - sort.Sort(byAttr(e.Attr)) -} - -type byAttr []Attr - -func (a byAttr) Len() int { - return len(a) -} - -func (a byAttr) Swap(i, j int) { - a[i], a[j] = a[j], a[i] -} - -func (a byAttr) Less(i, j int) bool { - sp := strings.Compare(a[i].Space, a[j].Space) - if sp == 0 { - return strings.Compare(a[i].Key, a[j].Key) < 0 - } - return sp < 0 + slices.SortFunc(e.Attr, func(a, b Attr) int { + if v := strings.Compare(a.Space, b.Space); v != 0 { + return v + } + return strings.Compare(a.Key, b.Key) + }) } -// FullKey returns the attribute a's complete key, including namespace prefix +// FullKey returns this attribute's complete key, including namespace prefix // if present. func (a *Attr) FullKey() string { if a.Space == "" { @@ -1110,43 +1461,56 @@ func (a *Attr) FullKey() string { return a.Space + ":" + a.Key } -// Element returns the element containing the attribute. +// Element returns a pointer to the element containing this attribute. func (a *Attr) Element() *Element { return a.element } -// NamespaceURI returns the XML namespace URI associated with the attribute. -// If the element is part of the XML default namespace, NamespaceURI returns -// the empty string. +// NamespaceURI returns the XML namespace URI associated with this attribute. +// The function returns the empty string if the attribute is unprefixed or +// if the attribute is part of the XML default namespace. func (a *Attr) NamespaceURI() string { - return a.element.NamespaceURI() + if a.Space == "" { + return "" + } + return a.element.findLocalNamespaceURI(a.Space) } -// writeTo serializes the attribute to the writer. -func (a *Attr) writeTo(w *bufio.Writer, s *WriteSettings) { +// WriteTo serializes the attribute to the writer. +func (a *Attr) WriteTo(w Writer, s *WriteSettings) { w.WriteString(a.FullKey()) - w.WriteString(`="`) + if s.AttrSingleQuote { + w.WriteString(`='`) + } else { + w.WriteString(`="`) + } var m escapeMode - if s.CanonicalAttrVal { + if s.CanonicalAttrVal && !s.AttrSingleQuote { m = escapeCanonicalAttr } else { m = escapeNormal } escapeString(w, a.Value, m) - w.WriteByte('"') + if s.AttrSingleQuote { + w.WriteByte('\'') + } else { + w.WriteByte('"') + } } -// NewText creates a parentless CharData token containing character data. +// NewText creates an unparented CharData token containing simple text data. func NewText(text string) *CharData { return newCharData(text, 0, nil) } -// NewCData creates a parentless XML character CDATA section. +// NewCData creates an unparented XML character CDATA section with 'data' as +// its content. func NewCData(data string) *CharData { return newCharData(data, cdataFlag, nil) } -// NewCharData creates a parentless CharData token containing character data. +// NewCharData creates an unparented CharData token containing simple text +// data. // // Deprecated: NewCharData is deprecated. Instead, use NewText, which does the // same thing. @@ -1159,7 +1523,7 @@ func NewCharData(data string) *CharData { func newCharData(data string, flags charDataFlags, parent *Element) *CharData { c := &CharData{ Data: data, - parent: parent, + parent: nil, index: -1, flags: flags, } @@ -1169,75 +1533,67 @@ func newCharData(data string, flags charDataFlags, parent *Element) *CharData { return c } -// CreateText creates a CharData token containing character data and adds it -// as a child of element e. +// CreateText creates a CharData token containing simple text data and adds it +// to the end of this element's list of child tokens. func (e *Element) CreateText(text string) *CharData { return newCharData(text, 0, e) } -// CreateCData creates a CharData token containing a CDATA section and adds it -// as a child of element e. +// CreateCData creates a CharData token containing a CDATA section with 'data' +// as its content and adds it to the end of this element's list of child +// tokens. func (e *Element) CreateCData(data string) *CharData { return newCharData(data, cdataFlag, e) } -// CreateCharData creates a CharData token containing character data and adds -// it as a child of element e. +// CreateCharData creates a CharData token containing simple text data and +// adds it to the end of this element's list of child tokens. // // Deprecated: CreateCharData is deprecated. Instead, use CreateText, which // does the same thing. func (e *Element) CreateCharData(data string) *CharData { - return newCharData(data, 0, e) + return e.CreateText(data) } -// dup duplicates the character data. -func (c *CharData) dup(parent *Element) Token { - return &CharData{ - Data: c.Data, - flags: c.flags, - parent: parent, - index: c.index, +// SetData modifies the content of the CharData token. In the case of a +// CharData token containing simple text, the simple text is modified. In the +// case of a CharData token containing a CDATA section, the CDATA section's +// content is modified. +func (c *CharData) SetData(text string) { + c.Data = text + if isWhitespace(text) { + c.flags |= whitespaceFlag + } else { + c.flags &= ^whitespaceFlag } } -// IsCData returns true if the character data token is to be encoded as a -// CDATA section. +// IsCData returns true if this CharData token is contains a CDATA section. It +// returns false if the CharData token contains simple text. func (c *CharData) IsCData() bool { return (c.flags & cdataFlag) != 0 } -// IsWhitespace returns true if the character data token was created by one of -// the document Indent methods to contain only whitespace. +// IsWhitespace returns true if this CharData token contains only whitespace. func (c *CharData) IsWhitespace() bool { return (c.flags & whitespaceFlag) != 0 } -// Parent returns the character data token's parent element, or nil if it has -// no parent. +// Parent returns this CharData token's parent element, or nil if it has no +// parent. func (c *CharData) Parent() *Element { return c.parent } // Index returns the index of this CharData token within its parent element's -// list of child tokens. If this CharData token has no parent element, the -// index is -1. +// list of child tokens. If this CharData token has no parent, then the +// function returns -1. func (c *CharData) Index() int { return c.index } -// setParent replaces the character data token's parent. -func (c *CharData) setParent(parent *Element) { - c.parent = parent -} - -// setIndex sets the CharData token's index within its parent element's Child -// slice. -func (c *CharData) setIndex(index int) { - c.index = index -} - -// writeTo serializes character data to the writer. -func (c *CharData) writeTo(w *bufio.Writer, s *WriteSettings) { +// WriteTo serializes character data to the writer. +func (c *CharData) WriteTo(w Writer, s *WriteSettings) { if c.IsCData() { w.WriteString(`<![CDATA[`) w.WriteString(c.Data) @@ -1253,17 +1609,37 @@ func (c *CharData) writeTo(w *bufio.Writer, s *WriteSettings) { } } -// NewComment creates a parentless XML comment. +// dup duplicates the character data. +func (c *CharData) dup(parent *Element) Token { + return &CharData{ + Data: c.Data, + flags: c.flags, + parent: parent, + index: c.index, + } +} + +// setParent replaces the character data token's parent. +func (c *CharData) setParent(parent *Element) { + c.parent = parent +} + +// setIndex sets the CharData token's index within its parent element's Child +// slice. +func (c *CharData) setIndex(index int) { + c.index = index +} + +// NewComment creates an unparented comment token. func NewComment(comment string) *Comment { return newComment(comment, nil) } -// NewComment creates an XML comment and binds it to a parent element. If -// parent is nil, the Comment remains unbound. +// NewComment creates a comment token and sets its parent element to 'parent'. func newComment(comment string, parent *Element) *Comment { c := &Comment{ Data: comment, - parent: parent, + parent: nil, index: -1, } if parent != nil { @@ -1272,7 +1648,8 @@ func newComment(comment string, parent *Element) *Comment { return c } -// CreateComment creates an XML comment and adds it as a child of element e. +// CreateComment creates a comment token using the specified 'comment' string +// and adds it as the last child token of this element. func (e *Element) CreateComment(comment string) *Comment { return newComment(comment, e) } @@ -1292,12 +1669,19 @@ func (c *Comment) Parent() *Element { } // Index returns the index of this Comment token within its parent element's -// list of child tokens. If this Comment token has no parent element, the -// index is -1. +// list of child tokens. If this Comment token has no parent, then the +// function returns -1. func (c *Comment) Index() int { return c.index } +// WriteTo serialies the comment to the writer. +func (c *Comment) WriteTo(w Writer, s *WriteSettings) { + w.WriteString("<!--") + w.WriteString(c.Data) + w.WriteString("-->") +} + // setParent replaces the comment token's parent. func (c *Comment) setParent(parent *Element) { c.parent = parent @@ -1309,14 +1693,7 @@ func (c *Comment) setIndex(index int) { c.index = index } -// writeTo serialies the comment to the writer. -func (c *Comment) writeTo(w *bufio.Writer, s *WriteSettings) { - w.WriteString("<!--") - w.WriteString(c.Data) - w.WriteString("-->") -} - -// NewDirective creates a parentless XML directive. +// NewDirective creates an unparented XML directive token. func NewDirective(data string) *Directive { return newDirective(data, nil) } @@ -1326,7 +1703,7 @@ func NewDirective(data string) *Directive { func newDirective(data string, parent *Element) *Directive { d := &Directive{ Data: data, - parent: parent, + parent: nil, index: -1, } if parent != nil { @@ -1335,8 +1712,8 @@ func newDirective(data string, parent *Element) *Directive { return d } -// CreateDirective creates an XML directive and adds it as the last child of -// element e. +// CreateDirective creates an XML directive token with the specified 'data' +// value and adds it as the last child token of this element. func (e *Element) CreateDirective(data string) *Directive { return newDirective(data, e) } @@ -1357,12 +1734,19 @@ func (d *Directive) Parent() *Element { } // Index returns the index of this Directive token within its parent element's -// list of child tokens. If this Directive token has no parent element, the -// index is -1. +// list of child tokens. If this Directive token has no parent, then the +// function returns -1. func (d *Directive) Index() int { return d.index } +// WriteTo serializes the XML directive to the writer. +func (d *Directive) WriteTo(w Writer, s *WriteSettings) { + w.WriteString("<!") + w.WriteString(d.Data) + w.WriteString(">") +} + // setParent replaces the directive token's parent. func (d *Directive) setParent(parent *Element) { d.parent = parent @@ -1374,14 +1758,7 @@ func (d *Directive) setIndex(index int) { d.index = index } -// writeTo serializes the XML directive to the writer. -func (d *Directive) writeTo(w *bufio.Writer, s *WriteSettings) { - w.WriteString("<!") - w.WriteString(d.Data) - w.WriteString(">") -} - -// NewProcInst creates a parentless XML processing instruction. +// NewProcInst creates an unparented XML processing instruction. func NewProcInst(target, inst string) *ProcInst { return newProcInst(target, inst, nil) } @@ -1392,7 +1769,7 @@ func newProcInst(target, inst string, parent *Element) *ProcInst { p := &ProcInst{ Target: target, Inst: inst, - parent: parent, + parent: nil, index: -1, } if parent != nil { @@ -1401,8 +1778,9 @@ func newProcInst(target, inst string, parent *Element) *ProcInst { return p } -// CreateProcInst creates a processing instruction and adds it as a child of -// element e. +// CreateProcInst creates an XML processing instruction token with the +// specified 'target' and instruction 'inst'. It is then added as the last +// child token of this element. func (e *Element) CreateProcInst(target, inst string) *ProcInst { return newProcInst(target, inst, e) } @@ -1424,12 +1802,23 @@ func (p *ProcInst) Parent() *Element { } // Index returns the index of this ProcInst token within its parent element's -// list of child tokens. If this ProcInst token has no parent element, the -// index is -1. +// list of child tokens. If this ProcInst token has no parent, then the +// function returns -1. func (p *ProcInst) Index() int { return p.index } +// WriteTo serializes the processing instruction to the writer. +func (p *ProcInst) WriteTo(w Writer, s *WriteSettings) { + w.WriteString("<?") + w.WriteString(p.Target) + if p.Inst != "" { + w.WriteByte(' ') + w.WriteString(p.Inst) + } + w.WriteString("?>") +} + // setParent replaces the processing instruction token's parent. func (p *ProcInst) setParent(parent *Element) { p.parent = parent @@ -1440,14 +1829,3 @@ func (p *ProcInst) setParent(parent *Element) { func (p *ProcInst) setIndex(index int) { p.index = index } - -// writeTo serializes the processing instruction to the writer. -func (p *ProcInst) writeTo(w *bufio.Writer, s *WriteSettings) { - w.WriteString("<?") - w.WriteString(p.Target) - if p.Inst != "" { - w.WriteByte(' ') - w.WriteString(p.Inst) - } - w.WriteString("?>") -} diff --git a/vendor/github.com/beevik/etree/helpers.go b/vendor/github.com/beevik/etree/helpers.go index 825e14e9..ea789b62 100644 --- a/vendor/github.com/beevik/etree/helpers.go +++ b/vendor/github.com/beevik/etree/helpers.go @@ -5,43 +5,41 @@ package etree import ( - "bufio" "io" "strings" "unicode/utf8" ) -// A simple stack -type stack struct { - data []interface{} +type stack[E any] struct { + data []E } -func (s *stack) empty() bool { +func (s *stack[E]) empty() bool { return len(s.data) == 0 } -func (s *stack) push(value interface{}) { +func (s *stack[E]) push(value E) { s.data = append(s.data, value) } -func (s *stack) pop() interface{} { +func (s *stack[E]) pop() E { value := s.data[len(s.data)-1] - s.data[len(s.data)-1] = nil + var empty E + s.data[len(s.data)-1] = empty s.data = s.data[:len(s.data)-1] return value } -func (s *stack) peek() interface{} { +func (s *stack[E]) peek() E { return s.data[len(s.data)-1] } -// A fifo is a simple first-in-first-out queue. -type fifo struct { - data []interface{} +type queue[E any] struct { + data []E head, tail int } -func (f *fifo) add(value interface{}) { +func (f *queue[E]) add(value E) { if f.len()+1 >= len(f.data) { f.grow() } @@ -51,70 +49,190 @@ func (f *fifo) add(value interface{}) { } } -func (f *fifo) remove() interface{} { +func (f *queue[E]) remove() E { value := f.data[f.head] - f.data[f.head] = nil + var empty E + f.data[f.head] = empty if f.head++; f.head == len(f.data) { f.head = 0 } return value } -func (f *fifo) len() int { +func (f *queue[E]) len() int { if f.tail >= f.head { return f.tail - f.head } return len(f.data) - f.head + f.tail } -func (f *fifo) grow() { +func (f *queue[E]) grow() { c := len(f.data) * 2 if c == 0 { c = 4 } - buf, count := make([]interface{}, c), f.len() + buf, count := make([]E, c), f.len() if f.tail >= f.head { - copy(buf[0:count], f.data[f.head:f.tail]) + copy(buf[:count], f.data[f.head:f.tail]) } else { hindex := len(f.data) - f.head - copy(buf[0:hindex], f.data[f.head:]) + copy(buf[:hindex], f.data[f.head:]) copy(buf[hindex:count], f.data[:f.tail]) } f.data, f.head, f.tail = buf, 0, count } -// countReader implements a proxy reader that counts the number of +// xmlReader provides the interface by which an XML byte stream is +// processed and decoded. +type xmlReader interface { + Bytes() int64 + Read(p []byte) (n int, err error) +} + +// xmlSimpleReader implements a proxy reader that counts the number of // bytes read from its encapsulated reader. -type countReader struct { +type xmlSimpleReader struct { r io.Reader bytes int64 } -func newCountReader(r io.Reader) *countReader { - return &countReader{r: r} +func newXmlSimpleReader(r io.Reader) xmlReader { + return &xmlSimpleReader{r, 0} +} + +func (xr *xmlSimpleReader) Bytes() int64 { + return xr.bytes +} + +func (xr *xmlSimpleReader) Read(p []byte) (n int, err error) { + n, err = xr.r.Read(p) + xr.bytes += int64(n) + return n, err +} + +// xmlPeekReader implements a proxy reader that counts the number of +// bytes read from its encapsulated reader. It also allows the caller to +// "peek" at the previous portions of the buffer after they have been +// parsed. +type xmlPeekReader struct { + r io.Reader + bytes int64 // total bytes read by the Read function + buf []byte // internal read buffer + bufSize int // total bytes used in the read buffer + bufOffset int64 // total bytes read when buf was last filled + window []byte // current read buffer window + peekBuf []byte // buffer used to store data to be peeked at later + peekOffset int64 // total read offset of the start of the peek buffer +} + +func newXmlPeekReader(r io.Reader) *xmlPeekReader { + buf := make([]byte, 4096) + return &xmlPeekReader{ + r: r, + bytes: 0, + buf: buf, + bufSize: 0, + bufOffset: 0, + window: buf[0:0], + peekBuf: make([]byte, 0), + peekOffset: -1, + } +} + +func (xr *xmlPeekReader) Bytes() int64 { + return xr.bytes +} + +func (xr *xmlPeekReader) Read(p []byte) (n int, err error) { + if len(xr.window) == 0 { + err = xr.fill() + if err != nil { + return 0, err + } + if len(xr.window) == 0 { + return 0, nil + } + } + + if len(xr.window) < len(p) { + n = len(xr.window) + } else { + n = len(p) + } + + copy(p, xr.window) + xr.window = xr.window[n:] + xr.bytes += int64(n) + + return n, err +} + +func (xr *xmlPeekReader) PeekPrepare(offset int64, maxLen int) { + if maxLen > cap(xr.peekBuf) { + xr.peekBuf = make([]byte, 0, maxLen) + } + xr.peekBuf = xr.peekBuf[0:0] + xr.peekOffset = offset + xr.updatePeekBuf() +} + +func (xr *xmlPeekReader) PeekFinalize() []byte { + xr.updatePeekBuf() + return xr.peekBuf } -func (cr *countReader) Read(p []byte) (n int, err error) { - b, err := cr.r.Read(p) - cr.bytes += int64(b) - return b, err +func (xr *xmlPeekReader) fill() error { + xr.bufOffset = xr.bytes + xr.bufSize = 0 + n, err := xr.r.Read(xr.buf) + if err != nil { + xr.window, xr.bufSize = xr.buf[0:0], 0 + return err + } + xr.window, xr.bufSize = xr.buf[:n], n + xr.updatePeekBuf() + return nil +} + +func (xr *xmlPeekReader) updatePeekBuf() { + peekRemain := cap(xr.peekBuf) - len(xr.peekBuf) + if xr.peekOffset >= 0 && peekRemain > 0 { + rangeMin := xr.peekOffset + rangeMax := xr.peekOffset + int64(cap(xr.peekBuf)) + bufMin := xr.bufOffset + bufMax := xr.bufOffset + int64(xr.bufSize) + if rangeMin < bufMin { + rangeMin = bufMin + } + if rangeMax > bufMax { + rangeMax = bufMax + } + if rangeMax > rangeMin { + rangeMin -= xr.bufOffset + rangeMax -= xr.bufOffset + if int(rangeMax-rangeMin) > peekRemain { + rangeMax = rangeMin + int64(peekRemain) + } + xr.peekBuf = append(xr.peekBuf, xr.buf[rangeMin:rangeMax]...) + } + } } -// countWriter implements a proxy writer that counts the number of +// xmlWriter implements a proxy writer that counts the number of // bytes written by its encapsulated writer. -type countWriter struct { +type xmlWriter struct { w io.Writer bytes int64 } -func newCountWriter(w io.Writer) *countWriter { - return &countWriter{w: w} +func newXmlWriter(w io.Writer) *xmlWriter { + return &xmlWriter{w: w} } -func (cw *countWriter) Write(p []byte) (n int, err error) { - b, err := cw.w.Write(p) - cw.bytes += int64(b) - return b, err +func (xw *xmlWriter) Write(p []byte) (n int, err error) { + n, err = xw.w.Write(p) + xw.bytes += int64(n) + return n, err } // isWhitespace returns true if the byte slice contains only @@ -181,10 +299,10 @@ func indentLF(n int, source string) string { } } -// nextIndex returns the index of the next occurrence of sep in s, -// starting from offset. It returns -1 if the sep string is not found. -func nextIndex(s, sep string, offset int) int { - switch i := strings.Index(s[offset:], sep); i { +// nextIndex returns the index of the next occurrence of byte ch in s, +// starting from offset. It returns -1 if the byte is not found. +func nextIndex(s string, ch byte, offset int) int { + switch i := strings.IndexByte(s[offset:], ch); i { case -1: return -1 default: @@ -211,7 +329,7 @@ const ( ) // escapeString writes an escaped version of a string to the writer. -func escapeString(w *bufio.Writer, s string, m escapeMode) { +func escapeString(w Writer, s string, m escapeMode) { var esc []byte last := 0 for i := 0; i < len(s); { diff --git a/vendor/github.com/beevik/etree/path.go b/vendor/github.com/beevik/etree/path.go index 82db0ac5..8d630960 100644 --- a/vendor/github.com/beevik/etree/path.go +++ b/vendor/github.com/beevik/etree/path.go @@ -19,66 +19,73 @@ be modified by one or more bracket-enclosed "filters". Selectors are used to traverse the etree from element to element, while filters are used to narrow the list of candidate elements at each node. -Although etree Path strings are similar to XPath strings -(https://www.w3.org/TR/1999/REC-xpath-19991116/), they have a more limited set -of selectors and filtering options. +Although etree Path strings are structurally and behaviorally similar to XPath +strings (https://www.w3.org/TR/1999/REC-xpath-19991116/), they have a more +limited set of selectors and filtering options. -The following selectors are supported by etree Path strings: +The following selectors are supported by etree paths: - . Select the current element. - .. Select the parent of the current element. - * Select all child elements of the current element. - / Select the root element when used at the start of a path. - // Select all descendants of the current element. - tag Select all child elements with a name matching the tag. + . Select the current element. + .. Select the parent of the current element. + * Select all child elements of the current element. + / Select the root element when used at the start of a path. + // Select all descendants of the current element. + tag Select all child elements with a name matching the tag. -The following basic filters are supported by etree Path strings: +The following basic filters are supported: - [@attrib] Keep elements with an attribute named attrib. - [@attrib='val'] Keep elements with an attribute named attrib and value matching val. - [tag] Keep elements with a child element named tag. - [tag='val'] Keep elements with a child element named tag and text matching val. - [n] Keep the n-th element, where n is a numeric index starting from 1. + [@attrib] Keep elements with an attribute named attrib. + [@attrib='val'] Keep elements with an attribute named attrib and value matching val. + [tag] Keep elements with a child element named tag. + [tag='val'] Keep elements with a child element named tag and text matching val. + [n] Keep the n-th element, where n is a numeric index starting from 1. -The following function filters are also supported: +The following function-based filters are supported: - [text()] Keep elements with non-empty text. - [text()='val'] Keep elements whose text matches val. - [local-name()='val'] Keep elements whose un-prefixed tag matches val. - [name()='val'] Keep elements whose full tag exactly matches val. - [namespace-prefix()='val'] Keep elements whose namespace prefix matches val. - [namespace-uri()='val'] Keep elements whose namespace URI matches val. + [text()] Keep elements with non-empty text. + [text()='val'] Keep elements whose text matches val. + [local-name()='val'] Keep elements whose un-prefixed tag matches val. + [name()='val'] Keep elements whose full tag exactly matches val. + [namespace-prefix()] Keep elements with non-empty namespace prefixes. + [namespace-prefix()='val'] Keep elements whose namespace prefix matches val. + [namespace-uri()] Keep elements with non-empty namespace URIs. + [namespace-uri()='val'] Keep elements whose namespace URI matches val. -Here are some examples of Path strings: +Below are some examples of etree path strings. -- Select the bookstore child element of the root element: - /bookstore +Select the bookstore child element of the root element: -- Beginning from the root element, select the title elements of all -descendant book elements having a 'category' attribute of 'WEB': - //book[@category='WEB']/title + /bookstore -- Beginning from the current element, select the first descendant -book element with a title child element containing the text 'Great -Expectations': - .//book[title='Great Expectations'][1] +Beginning from the root element, select the title elements of all descendant +book elements having a 'category' attribute of 'WEB': -- Beginning from the current element, select all child elements of -book elements with an attribute 'language' set to 'english': - ./book/*[@language='english'] + //book[@category='WEB']/title -- Beginning from the current element, select all child elements of -book elements containing the text 'special': - ./book/*[text()='special'] +Beginning from the current element, select the first descendant book element +with a title child element containing the text 'Great Expectations': -- Beginning from the current element, select all descendant book -elements whose title child element has a 'language' attribute of 'french': - .//book/title[@language='french']/.. + .//book[title='Great Expectations'][1] -- Beginning from the current element, select all book elements +Beginning from the current element, select all child elements of book elements +with an attribute 'language' set to 'english': + + ./book/*[@language='english'] + +Beginning from the current element, select all child elements of book elements +containing the text 'special': + + ./book/*[text()='special'] + +Beginning from the current element, select all descendant book elements whose +title child element has a 'language' attribute of 'french': + + .//book/title[@language='french']/.. + +Beginning from the current element, select all descendant book elements belonging to the http://www.w3.org/TR/html4/ namespace: - .//book[namespace-uri()='http://www.w3.org/TR/html4/'] + .//book[namespace-uri()='http://www.w3.org/TR/html4/'] */ type Path struct { segments []segment @@ -145,7 +152,7 @@ type filter interface { // a Path object. It collects and deduplicates all elements matching // the path query. type pather struct { - queue fifo + queue queue[node] results []*Element inResults map[*Element]bool candidates []*Element @@ -173,12 +180,12 @@ func newPather() *pather { // and filters. func (p *pather) traverse(e *Element, path Path) []*Element { for p.queue.add(node{e, path.segments}); p.queue.len() > 0; { - p.eval(p.queue.remove().(node)) + p.eval(p.queue.remove()) } return p.results } -// eval evalutes the current path node by applying the remaining +// eval evaluates the current path node by applying the remaining // path's selector rules against the node's element. func (p *pather) eval(n node) { p.candidates = p.candidates[0:0] @@ -210,7 +217,7 @@ type compiler struct { func (c *compiler) parsePath(path string) []segment { // If path ends with //, fix it if strings.HasSuffix(path, "//") { - path = path + "*" + path += "*" } var segments []segment @@ -232,15 +239,20 @@ func (c *compiler) parsePath(path string) []segment { } func splitPath(path string) []string { - pieces := make([]string, 0) + var pieces []string start := 0 inquote := false + var quote byte for i := 0; i+1 <= len(path); i++ { - if path[i] == '\'' { - inquote = !inquote - } else if path[i] == '/' && !inquote { - pieces = append(pieces, path[start:i]) - start = i + 1 + if !inquote { + if path[i] == '\'' || path[i] == '"' { + inquote, quote = true, path[i] + } else if path[i] == '/' { + pieces = append(pieces, path[start:i]) + start = i + 1 + } + } else if path[i] == quote { + inquote = false } } return append(pieces, path[start:]) @@ -255,7 +267,7 @@ func (c *compiler) parseSegment(path string) segment { } for i := 1; i < len(pieces); i++ { fpath := pieces[i] - if fpath[len(fpath)-1] != ']' { + if len(fpath) == 0 || fpath[len(fpath)-1] != ']' { c.err = ErrPath("path has invalid filter [brackets].") break } @@ -280,15 +292,12 @@ func (c *compiler) parseSelector(path string) selector { } } -var fnTable = map[string]struct { - hasFn func(e *Element) bool - getValFn func(e *Element) string -}{ - "local-name": {nil, (*Element).name}, - "name": {nil, (*Element).FullTag}, - "namespace-prefix": {nil, (*Element).namespacePrefix}, - "namespace-uri": {nil, (*Element).NamespaceURI}, - "text": {(*Element).hasText, (*Element).Text}, +var fnTable = map[string]func(e *Element) string{ + "local-name": (*Element).name, + "name": (*Element).FullTag, + "namespace-prefix": (*Element).namespacePrefix, + "namespace-uri": (*Element).NamespaceURI, + "text": (*Element).Text, } // parseFilter parses a path filter contained within [brackets]. @@ -298,30 +307,34 @@ func (c *compiler) parseFilter(path string) filter { return nil } - // Filter contains [@attr='val'], [fn()='val'], or [tag='val']? - eqindex := strings.Index(path, "='") - if eqindex >= 0 { - rindex := nextIndex(path, "'", eqindex+2) - if rindex != len(path)-1 { - c.err = ErrPath("path has mismatched filter quotes.") - return nil - } - - key := path[:eqindex] - value := path[eqindex+2 : rindex] + // Filter contains [@attr='val'], [@attr="val"], [fn()='val'], + // [fn()="val"], [tag='val'] or [tag="val"]? + eqindex := strings.IndexByte(path, '=') + if eqindex >= 0 && eqindex+1 < len(path) { + quote := path[eqindex+1] + if quote == '\'' || quote == '"' { + rindex := nextIndex(path, quote, eqindex+2) + if rindex != len(path)-1 { + c.err = ErrPath("path has mismatched filter quotes.") + return nil + } - switch { - case key[0] == '@': - return newFilterAttrVal(key[1:], value) - case strings.HasSuffix(key, "()"): - fn := key[:len(key)-2] - if t, ok := fnTable[fn]; ok && t.getValFn != nil { - return newFilterFuncVal(t.getValFn, value) + key := path[:eqindex] + value := path[eqindex+2 : rindex] + + switch { + case key[0] == '@': + return newFilterAttrVal(key[1:], value) + case strings.HasSuffix(key, "()"): + name := key[:len(key)-2] + if fn, ok := fnTable[name]; ok { + return newFilterFuncVal(fn, value) + } + c.err = ErrPath("path has unknown function " + name) + return nil + default: + return newFilterChildText(key, value) } - c.err = ErrPath("path has unknown function " + fn) - return nil - default: - return newFilterChildText(key, value) } } @@ -330,11 +343,11 @@ func (c *compiler) parseFilter(path string) filter { case path[0] == '@': return newFilterAttr(path[1:]) case strings.HasSuffix(path, "()"): - fn := path[:len(path)-2] - if t, ok := fnTable[fn]; ok && t.hasFn != nil { - return newFilterFunc(t.hasFn) + name := path[:len(path)-2] + if fn, ok := fnTable[name]; ok { + return newFilterFunc(fn) } - c.err = ErrPath("path has unknown function " + fn) + c.err = ErrPath("path has unknown function " + name) return nil case isInteger(path): pos, _ := strconv.Atoi(path) @@ -393,9 +406,9 @@ func (s *selectChildren) apply(e *Element, p *pather) { type selectDescendants struct{} func (s *selectDescendants) apply(e *Element, p *pather) { - var queue fifo + var queue queue[*Element] for queue.add(e); queue.len() > 0; { - e := queue.remove().(*Element) + e := queue.remove() p.candidates = append(p.candidates, e) for _, c := range e.Child { if c, ok := c.(*Element); ok { @@ -496,16 +509,16 @@ func (f *filterAttrVal) apply(p *pather) { // filterFunc filters the candidate list for elements satisfying a custom // boolean function. type filterFunc struct { - fn func(e *Element) bool + fn func(e *Element) string } -func newFilterFunc(fn func(e *Element) bool) *filterFunc { +func newFilterFunc(fn func(e *Element) string) *filterFunc { return &filterFunc{fn} } func (f *filterFunc) apply(p *pather) { for _, c := range p.candidates { - if f.fn(c) { + if f.fn(c) != "" { p.scratch = append(p.scratch, c) } } diff --git a/vendor/github.com/crewjam/saml/.golangci.yml b/vendor/github.com/crewjam/saml/.golangci.yml index 23f37cbf..07b7f07a 100644 --- a/vendor/github.com/crewjam/saml/.golangci.yml +++ b/vendor/github.com/crewjam/saml/.golangci.yml @@ -1,72 +1,70 @@ -# Configuration file for golangci-lint -# -# https://github.com/golangci/golangci-lint -# -# fighting with false positives? -# https://github.com/golangci/golangci-lint#nolint - +version: "2" linters: enable: - - bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false] - - errcheck # Inspects source code for security problems [fast: true, auto-fix: false] - - gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false] - - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] - - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] - - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true] - - gosec # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false] - - gosimple # Linter for Go source code that specializes in simplifying a code [fast: false, auto-fix: false] - - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false] - - ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] - - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] - - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] - - prealloc # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false] - - revive # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true, auto-fix: false] - - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false] - - stylecheck # Stylecheck is a replacement for golint [fast: false, auto-fix: false] - - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false] - - unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false] - - unparam # Reports unused function parameters [fast: false, auto-fix: false] - - unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] - + - bodyclose + - gocritic + - gocyclo + - gosec + - misspell + - nakedret + - prealloc + - revive + - staticcheck + - unconvert + - unparam disable: # TODO(ross): fix errors reported by these checkers and enable them - - dupl # Tool for code clone detection [fast: true, auto-fix: false] - - gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false] - - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] - - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] - - lll # Reports long lines [fast: true, auto-fix: false] - - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] -linters-settings: - goimports: - local-prefixes: github.com/crewjam/saml - govet: - disable: - - shadow - enable: - - asmdecl - - assign - - atomic - - bools - - buildtag - - cgocall - - composites - - copylocks - - errorsas - - httpresponse - - loopclosure - - lostcancel - - nilfunc - - printf - - shift - - stdmethods - - structtag - - tests - - unmarshal - - unreachable - - unsafeptr - - unusedresult -issues: - exclude-use-default: false - exclude: - - G104 # 'Errors unhandled. (gosec) - + - depguard + - dupl + - gochecknoglobals + - gochecknoinits + - goconst + - lll + settings: + govet: + enable: + - asmdecl + - assign + - atomic + - bools + - buildtag + - cgocall + - composites + - copylocks + - errorsas + - httpresponse + - loopclosure + - lostcancel + - nilfunc + - printf + - shift + - stdmethods + - structtag + - tests + - unmarshal + - unreachable + - unsafeptr + - unusedresult + disable: + - shadow + exclusions: + generated: lax + rules: + - path: (.+)\.go$ + text: G104 # 'Errors unhandled. (gosec) + paths: + - example/.*\.go$ +formatters: + enable: + - gofmt + - goimports + settings: + goimports: + local-prefixes: + - github.com/crewjam/saml + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/vendor/github.com/crewjam/saml/CODEOWNERS b/vendor/github.com/crewjam/saml/CODEOWNERS new file mode 100644 index 00000000..6da4fdeb --- /dev/null +++ b/vendor/github.com/crewjam/saml/CODEOWNERS @@ -0,0 +1 @@ +* @crewjam diff --git a/vendor/github.com/crewjam/saml/README.md b/vendor/github.com/crewjam/saml/README.md index c0b98058..1a435834 100644 --- a/vendor/github.com/crewjam/saml/README.md +++ b/vendor/github.com/crewjam/saml/README.md @@ -2,7 +2,7 @@ [](http://godoc.org/github.com/crewjam/saml) - + Package saml contains a partial implementation of the SAML standard in golang. SAML is a standard for identity federation, i.e. either allowing a third party to authenticate your users or allowing third parties to rely on us to authenticate their users. @@ -130,7 +130,7 @@ The SAML standard is huge and complex with many dark corners and strange, unused This package supports the **Web SSO** profile. Message flows from the service provider to the IDP are supported using the **HTTP Redirect** binding and the **HTTP POST** binding. Message flows from the IDP to the service provider are supported via the **HTTP POST** binding. -The package can produce signed SAML assertions, and can validate both signed and encrypted SAML assertions. It does not support signed or encrypted requests. +The package can produce signed SAML assertions, and can validate both signed and encrypted SAML assertions. ## RelayState diff --git a/vendor/github.com/crewjam/saml/identity_provider.go b/vendor/github.com/crewjam/saml/identity_provider.go index abaaad68..a0ad2e69 100644 --- a/vendor/github.com/crewjam/saml/identity_provider.go +++ b/vendor/github.com/crewjam/saml/identity_provider.go @@ -8,13 +8,13 @@ import ( "encoding/base64" "encoding/xml" "fmt" + "html/template" "io" "net/http" "net/url" "os" "regexp" "strconv" - "text/template" "time" "github.com/beevik/etree" @@ -38,13 +38,14 @@ type Session struct { NameIDFormat string SubjectID string - Groups []string - UserName string - UserEmail string - UserCommonName string - UserSurname string - UserGivenName string - UserScopedAffiliation string + Groups []string + UserName string + UserEmail string + UserCommonName string + UserSurname string + UserGivenName string + UserScopedAffiliation string + EduPersonPrincipalName string `json:",omitempty"` CustomAttributes []Attribute } @@ -101,12 +102,14 @@ type IdentityProvider struct { Intermediates []*x509.Certificate MetadataURL url.URL SSOURL url.URL + LoginURL url.URL LogoutURL url.URL ServiceProviderProvider ServiceProviderProvider SessionProvider SessionProvider AssertionMaker AssertionMaker SignatureMethod string ValidDuration *time.Duration + ResponseFormTemplate *template.Template } // Metadata returns the metadata structure for this identity provider. @@ -175,7 +178,7 @@ func (idp *IdentityProvider) Metadata() *EntityDescriptor { } if idp.LogoutURL.String() != "" { - ed.IDPSSODescriptors[0].SSODescriptor.SingleLogoutServices = []Endpoint{ + ed.IDPSSODescriptors[0].SingleLogoutServices = []Endpoint{ { Binding: HTTPRedirectBinding, Location: idp.LogoutURL.String(), @@ -662,13 +665,33 @@ func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Sessio } if session.UserEmail != "" { + attributes = append(attributes, Attribute{ + FriendlyName: "mail", + Name: "urn:oid:0.9.2342.19200300.100.1.3", + NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + Values: []AttributeValue{{ + Type: "xs:string", + Value: session.UserEmail, + }}, + }) + } + if session.EduPersonPrincipalName != "" || session.UserEmail != "" { + value := session.EduPersonPrincipalName + if value == "" { + // We used to set eduPersonPrincipalName (urn:oid:1.3.6.1.4.1.5923.1.1.1.6) + // to the value of session.UserEmail. It is more correct to set + // mail (urn:oid:0.9.2342.19200300.100.1.3). To avoid breaking things, + // we preserve the former behavior. + value = session.UserEmail + } + attributes = append(attributes, Attribute{ FriendlyName: "eduPersonPrincipalName", Name: "urn:oid:1.3.6.1.4.1.5923.1.1.1.6", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", Values: []AttributeValue{{ Type: "xs:string", - Value: session.UserEmail, + Value: value, }}, }) } @@ -709,7 +732,7 @@ func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Sessio if session.UserScopedAffiliation != "" { attributes = append(attributes, Attribute{ - FriendlyName: "uid", + FriendlyName: "scopedAffiliation", Name: "urn:oid:1.3.6.1.4.1.5923.1.1.1.9", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", Values: []AttributeValue{{ @@ -921,6 +944,16 @@ func (req *IdpAuthnRequest) PostBinding() (IdpAuthnRequestForm, error) { return form, nil } +var defaultResponseFormTemplate = template.Must(template.New("saml-post-form").Parse(`<html>` + + `<form method="post" action="{{.URL}}" id="SAMLResponseForm">` + + `<input type="hidden" name="SAMLResponse" value="{{.SAMLResponse}}" />` + + `<input type="hidden" name="RelayState" value="{{.RelayState}}" />` + + `<input id="SAMLSubmitButton" type="submit" value="Continue" />` + + `</form>` + + `<script>document.getElementById('SAMLSubmitButton').style.visibility='hidden';</script>` + + `<script>document.getElementById('SAMLResponseForm').submit();</script>` + + `</html>`)) + // WriteResponse writes the `Response` to the http.ResponseWriter. If // `Response` is not already set, it calls MakeResponse to produce it. func (req *IdpAuthnRequest) WriteResponse(w http.ResponseWriter) error { @@ -929,15 +962,10 @@ func (req *IdpAuthnRequest) WriteResponse(w http.ResponseWriter) error { return err } - tmpl := template.Must(template.New("saml-post-form").Parse(`<html>` + - `<form method="post" action="{{.URL}}" id="SAMLResponseForm">` + - `<input type="hidden" name="SAMLResponse" value="{{.SAMLResponse}}" />` + - `<input type="hidden" name="RelayState" value="{{.RelayState}}" />` + - `<input id="SAMLSubmitButton" type="submit" value="Continue" />` + - `</form>` + - `<script>document.getElementById('SAMLSubmitButton').style.visibility='hidden';</script>` + - `<script>document.getElementById('SAMLResponseForm').submit();</script>` + - `</html>`)) + tmpl := req.IDP.ResponseFormTemplate + if tmpl == nil { + tmpl = defaultResponseFormTemplate + } buf := bytes.NewBuffer(nil) if err := tmpl.Execute(buf, form); err != nil { diff --git a/vendor/github.com/crewjam/saml/metadata.go b/vendor/github.com/crewjam/saml/metadata.go index 006a9e67..d160ccc3 100644 --- a/vendor/github.com/crewjam/saml/metadata.go +++ b/vendor/github.com/crewjam/saml/metadata.go @@ -38,6 +38,55 @@ type EntitiesDescriptor struct { EntityDescriptors []EntityDescriptor `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"` } +// MarshalXML implements xml.Marshaler +func (m EntitiesDescriptor) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { + var validUntil *RelaxedTime + var cacheDuration *Duration + if m.ValidUntil != nil { + vu := RelaxedTime(*m.ValidUntil) + validUntil = &vu + } + if m.CacheDuration != nil { + cd := Duration(*m.CacheDuration) + cacheDuration = &cd + } + type Alias EntitiesDescriptor + aux := &struct { + ValidUntil *RelaxedTime `xml:"validUntil,attr,omitempty"` + CacheDuration *Duration `xml:"cacheDuration,attr,omitempty"` + *Alias + }{ + ValidUntil: validUntil, + CacheDuration: cacheDuration, + Alias: (*Alias)(&m), + } + return e.Encode(aux) +} + +// UnmarshalXML implements xml.Unmarshaler +func (m *EntitiesDescriptor) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + type Alias EntitiesDescriptor + aux := &struct { + ValidUntil *RelaxedTime `xml:"validUntil,attr,omitempty"` + CacheDuration *Duration `xml:"cacheDuration,attr,omitempty"` + *Alias + }{ + Alias: (*Alias)(m), + } + if err := d.DecodeElement(aux, &start); err != nil { + return err + } + if aux.ValidUntil != nil { + t := time.Time(*aux.ValidUntil) + m.ValidUntil = &t + } + if aux.CacheDuration != nil { + d := time.Duration(*aux.CacheDuration) + m.CacheDuration = &d + } + return nil +} + // Metadata as been renamed to EntityDescriptor // // This change was made to be consistent with the rest of the API which uses names diff --git a/vendor/github.com/crewjam/saml/saml.go b/vendor/github.com/crewjam/saml/saml.go index b171e56d..e6330637 100644 --- a/vendor/github.com/crewjam/saml/saml.go +++ b/vendor/github.com/crewjam/saml/saml.go @@ -149,7 +149,7 @@ // // This package supports the Web SSO profile. Message flows from the service provider to the IDP are supported using the HTTP Redirect binding and the HTTP POST binding. Message flows from the IDP to the service provider are supported via the HTTP POST binding. // -// The package can produce signed SAML assertions, and can validate both signed and encrypted SAML assertions. It does not support signed or encrypted requests. +// The package can produce signed SAML assertions, and can validate both signed and encrypted SAML assertions. // // # RelayState // diff --git a/vendor/github.com/crewjam/saml/schema.go b/vendor/github.com/crewjam/saml/schema.go index 23cddbca..1a543de2 100644 --- a/vendor/github.com/crewjam/saml/schema.go +++ b/vendor/github.com/crewjam/saml/schema.go @@ -353,12 +353,15 @@ func (r *ArtifactResolve) Element() *etree.Element { if r.Issuer != nil { el.AddChild(r.Issuer.Element()) } - artifact := etree.NewElement("samlp:Artifact") - artifact.SetText(r.Artifact) - el.AddChild(artifact) if r.Signature != nil { + // ADFS requires that <Signature> come before <Artifact>. + // ref: https://github.com/crewjam/saml/issues/535 + // ref: https://www.wiktorzychla.com/2017/09/adfs-and-saml2-artifact-binding-woes.html el.AddChild(r.Signature) } + artifact := etree.NewElement("samlp:Artifact") + artifact.SetText(r.Artifact) + el.AddChild(artifact) return el } diff --git a/vendor/github.com/crewjam/saml/service_provider.go b/vendor/github.com/crewjam/saml/service_provider.go index 30b35670..6c72444e 100644 --- a/vendor/github.com/crewjam/saml/service_provider.go +++ b/vendor/github.com/crewjam/saml/service_provider.go @@ -4,7 +4,11 @@ import ( "bytes" "compress/flate" "context" + "crypto" + "crypto/ecdsa" "crypto/rsa" + "crypto/sha256" + "crypto/sha512" "crypto/tls" "crypto/x509" "encoding/base64" @@ -16,6 +20,7 @@ import ( "net/http" "net/url" "regexp" + "strings" "time" "github.com/beevik/etree" @@ -66,8 +71,9 @@ type ServiceProvider struct { // Entity ID is optional - if not specified then MetadataURL will be used EntityID string - // Key is the RSA private key we use to sign requests. - Key *rsa.PrivateKey + // Key is private key we use to sign requests. It must be either an + // *rsa.PrivateKey or an *ecdsa.PrivateKey. + Key crypto.Signer // Certificate is the RSA public part of Key. Certificate *x509.Certificate @@ -91,6 +97,18 @@ type ServiceProvider struct { // IDPMetadata is the metadata from the identity provider. IDPMetadata *EntityDescriptor + // IDPCertificateFingerprint is fingerprint of the idp public certificate. If this field is specified, + // IDPCertificateFingerprintAlgorithm must also be specified, and IDPCertificate must not be specified. + IDPCertificateFingerprint *string + // IDPCertificateFingerprintAlgorithm is fingerprint algorithm used to obtain fingerprint of the idp public + // certificate. + // If this field is specified, IDPCertificateFingerprint must also be specified, and IDPCertificate must not be specified. + IDPCertificateFingerprintAlgorithm *string + + // IDPCertificate to use as idp public certificate. If this field is specified, IDPCertificateFingerprint and + // IDPCertificateFingerprintAlgorithm must not be specified. + IDPCertificate *string + // AuthnNameIDFormat is the format used in the NameIDPolicy for // authentication requests AuthnNameIDFormat NameIDFormat @@ -117,12 +135,30 @@ type ServiceProvider struct { // to verify signatures. SignatureVerifier SignatureVerifier - // SignatureMethod, if non-empty, authentication requests will be signed + // SignatureMethod, if non-empty, authentication requests will be signed. + // + // The method specified here must be consistent with the type of Key. + // + // If Key is *rsa.PrivateKey, then this must be one of dsig.RSASHA1SignatureMethod, + // dsig.RSASHA256SignatureMethod, dsig.RSASHA384SignatureMethod, or + // dsig.RSASHA512SignatureMethod: + // + // If Key is *ecdsa.PrivateKey, then this must be one of dsig.ECDSASHA1SignatureMethod, + // dsig.ECDSASHA256SignatureMethod, dsig.ECDSASHA384SignatureMethod, or + // dsig.ECDSASHA512SignatureMethod. SignatureMethod string // LogoutBindings specify the bindings available for SLO endpoint. If empty, // HTTP-POST binding is used. LogoutBindings []string + + // ValidateAudienceRestriction allows you to override the default audience validation + // for an assertion. If nil, the default audience validation is used. + ValidateAudienceRestriction func(assertion *Assertion) error + + // ValidateRequestID allows you to override the default request ID validation. + // If nil, the default request ID validation is used. + ValidateRequestID func(response Response, possibleRequestIDs []string) error } // MaxIssueDelay is the longest allowed time between when a SAML assertion is @@ -247,28 +283,32 @@ func (sp *ServiceProvider) MakeRedirectAuthenticationRequest(relayState string) // Redirect returns a URL suitable for using the redirect binding with the request func (r *AuthnRequest) Redirect(relayState string, sp *ServiceProvider) (*url.URL, error) { - w := &bytes.Buffer{} - w1 := base64.NewEncoder(base64.StdEncoding, w) - w2, _ := flate.NewWriter(w1, 9) + var requestStr strings.Builder + base64Writer := base64.NewEncoder(base64.StdEncoding, &requestStr) + compressedWriter, _ := flate.NewWriter(base64Writer, 9) doc := etree.NewDocument() doc.SetRoot(r.Element()) - if _, err := doc.WriteTo(w2); err != nil { - panic(err) + if _, err := doc.WriteTo(compressedWriter); err != nil { + return nil, err } - if err := w2.Close(); err != nil { - panic(err) + if err := compressedWriter.Close(); err != nil { + return nil, err } - if err := w1.Close(); err != nil { - panic(err) + if err := base64Writer.Close(); err != nil { + return nil, err + } + + rv, err := url.Parse(r.Destination) + if err != nil { + return nil, err } - rv, _ := url.Parse(r.Destination) // We can't depend on Query().set() as order matters for signing query := rv.RawQuery if len(query) > 0 { - query += "&SAMLRequest=" + url.QueryEscape(w.String()) + query += "&SAMLRequest=" + url.QueryEscape(requestStr.String()) } else { - query += "SAMLRequest=" + url.QueryEscape(w.String()) + query += "SAMLRequest=" + url.QueryEscape(requestStr.String()) } if relayState != "" { @@ -378,6 +418,85 @@ func (sp *ServiceProvider) getIDPSigningCerts() ([]*x509.Certificate, error) { return certs, nil } +func (sp *ServiceProvider) getCertBasedOnFingerprint(el *etree.Element) ([]*x509.Certificate, error) { + x509CertEl := el.FindElement("./Signature/KeyInfo/X509Data/X509Certificate") + if x509CertEl == nil { + return nil, fmt.Errorf("cannot validate signature on %s: no certificate present", el.Tag) + } + if len(x509CertEl.Child) != 1 { + return nil, fmt.Errorf("cannot validate signature on %s: x509 cert el child len != 1: %d", el.Tag, len(x509CertEl.Child)) + } + + x509CertElCharData, ok := x509CertEl.Child[0].(*etree.CharData) + if !ok { + return nil, fmt.Errorf("cannot validate signature on %s: x509 cert el first child not char data: %T", el.Tag, x509CertEl.Child[0]) + } + + cert, err := parseCert(x509CertElCharData.Data) + if err != nil { + return nil, fmt.Errorf("cannot validate signature on %s: %w", el.Tag, err) + } + + finP, err := fingerprint(cert, *sp.IDPCertificateFingerprintAlgorithm) + if err != nil { + return nil, fmt.Errorf("cannot validate signature on %s: %w", el.Tag, err) + } + + if *sp.IDPCertificateFingerprint != finP { + return nil, fmt.Errorf("cannot validate signature on %s: fingerprint mismatch", el.Tag) + } + + return []*x509.Certificate{cert}, nil + +} + +func parseCert(x509Data string) (*x509.Certificate, error) { + // cleanup whitespace + regex := regexp.MustCompile(`\s+`) + certStr := regex.ReplaceAllString(x509Data, "") + certBytes, err := base64.StdEncoding.DecodeString(certStr) + if err != nil { + return nil, fmt.Errorf("parse cert, cannot base64 decode cert string: %w", err) + } + + parsedCert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, fmt.Errorf("parse cert, cannot parse certificate: %w", err) + } + + return parsedCert, nil +} + +func fingerprint(cert *x509.Certificate, fingerprintAlgorithm string) (string, error) { + switch fingerprintAlgorithm { + case "http://www.w3.org/2001/04/xmlenc#sha256": + fp := sha256.Sum256(cert.Raw) + return fingerprintFormat(fp[:]) + case "http://www.w3.org/2001/04/xmlenc#sha512": + fp := sha512.Sum512(cert.Raw) + return fingerprintFormat(fp[:]) + default: + return "", fmt.Errorf("fingerprint, unknown algorithm: %s", fingerprintAlgorithm) + } +} + +func fingerprintFormat(fp []byte) (string, error) { + var buf bytes.Buffer + for i, f := range fp { + if i > 0 { + _, err := fmt.Fprintf(&buf, ":") + if err != nil { + return "", fmt.Errorf("fingerprint format, print ':': %w", err) + } + } + _, err := fmt.Fprintf(&buf, "%02X", f) + if err != nil { + return "", fmt.Errorf("fingerprint format, print bytes: %w", err) + } + } + return buf.String(), nil +} + // MakeArtifactResolveRequest produces a new ArtifactResolve object to send to the idp's Artifact resolver func (sp *ServiceProvider) MakeArtifactResolveRequest(artifactID string) (*ArtifactResolve, error) { req := ArtifactResolve{ @@ -447,17 +566,38 @@ func GetSigningContext(sp *ServiceProvider) (*dsig.SigningContext, error) { // for _, cert := range sp.Intermediates { // keyPair.Certificate = append(keyPair.Certificate, cert.Raw) // } - keyStore := dsig.TLSCertKeyStore(keyPair) - if sp.SignatureMethod != dsig.RSASHA1SignatureMethod && - sp.SignatureMethod != dsig.RSASHA256SignatureMethod && - sp.SignatureMethod != dsig.RSASHA512SignatureMethod { + switch sp.SignatureMethod { + case dsig.RSASHA1SignatureMethod, + dsig.RSASHA256SignatureMethod, + dsig.RSASHA384SignatureMethod, + dsig.RSASHA512SignatureMethod: + if _, ok := sp.Key.(*rsa.PrivateKey); !ok { + return nil, fmt.Errorf("signature method %s requires a key of type rsa.PrivateKey, not %T", sp.SignatureMethod, sp.Key) + } + + case dsig.ECDSASHA1SignatureMethod, + dsig.ECDSASHA256SignatureMethod, + dsig.ECDSASHA384SignatureMethod, + dsig.ECDSASHA512SignatureMethod: + if _, ok := sp.Key.(*ecdsa.PrivateKey); !ok { + return nil, fmt.Errorf("signature method %s requires a key of type ecdsa.PrivateKey, not %T", sp.SignatureMethod, sp.Key) + } + default: return nil, fmt.Errorf("invalid signing method %s", sp.SignatureMethod) } - signatureMethod := sp.SignatureMethod - signingContext := dsig.NewDefaultSigningContext(keyStore) + + keyStore := dsig.TLSCertKeyStore(keyPair) + chain, err := keyStore.GetChain() + if err != nil { + return nil, err + } + signingContext, err := dsig.NewSigningContext(sp.Key, chain) + if err != nil { + return nil, err + } signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList(canonicalizerPrefixList) - if err := signingContext.SetSignatureMethod(signatureMethod); err != nil { + if err := signingContext.SetSignatureMethod(sp.SignatureMethod); err != nil { return nil, err } @@ -652,7 +792,7 @@ func (sp *ServiceProvider) handleArtifactRequest(ctx context.Context, artifactID retErr.PrivateErr = fmt.Errorf("Error during artifact resolution: %s", err) return nil, retErr } - assertion, err := sp.ParseXMLArtifactResponse(responseBody, possibleRequestIDs, artifactResolveRequest.ID) + assertion, err := sp.ParseXMLArtifactResponse(responseBody, possibleRequestIDs, artifactResolveRequest.ID, *req.URL) if err != nil { return nil, err } @@ -670,7 +810,7 @@ func (sp *ServiceProvider) parseResponseHTTP(req *http.Request, possibleRequestI return nil, retErr } - assertion, err := sp.ParseXMLResponse(rawResponseBuf, possibleRequestIDs) + assertion, err := sp.ParseXMLResponse(rawResponseBuf, possibleRequestIDs, *req.URL) if err != nil { return nil, err } @@ -687,7 +827,7 @@ func (sp *ServiceProvider) parseResponseHTTP(req *http.Request, possibleRequestI // properties are useful in describing which part of the parsing process // failed. However, to discourage inadvertent disclosure the diagnostic // information, the Error() method returns a static string. -func (sp *ServiceProvider) ParseXMLArtifactResponse(soapResponseXML []byte, possibleRequestIDs []string, artifactRequestID string) (*Assertion, error) { +func (sp *ServiceProvider) ParseXMLArtifactResponse(soapResponseXML []byte, possibleRequestIDs []string, artifactRequestID string, currentURL url.URL) (*Assertion, error) { now := TimeNow() retErr := &InvalidResponseError{ Response: string(soapResponseXML), @@ -727,10 +867,10 @@ func (sp *ServiceProvider) ParseXMLArtifactResponse(soapResponseXML []byte, poss return nil, retErr } - return sp.parseArtifactResponse(artifactResponseEl, possibleRequestIDs, artifactRequestID, now) + return sp.parseArtifactResponse(artifactResponseEl, possibleRequestIDs, artifactRequestID, now, currentURL) } -func (sp *ServiceProvider) parseArtifactResponse(artifactResponseEl *etree.Element, possibleRequestIDs []string, artifactRequestID string, now time.Time) (*Assertion, error) { +func (sp *ServiceProvider) parseArtifactResponse(artifactResponseEl *etree.Element, possibleRequestIDs []string, artifactRequestID string, now time.Time, currentURL url.URL) (*Assertion, error) { retErr := &InvalidResponseError{ Now: now, Response: elementToString(artifactResponseEl), @@ -778,7 +918,7 @@ func (sp *ServiceProvider) parseArtifactResponse(artifactResponseEl *etree.Eleme return nil, retErr } - assertion, err := sp.parseResponse(responseEl, possibleRequestIDs, now, signatureRequirement) + assertion, err := sp.parseResponse(responseEl, possibleRequestIDs, now, signatureRequirement, currentURL) if err != nil { retErr.PrivateErr = err return nil, retErr @@ -798,7 +938,7 @@ func (sp *ServiceProvider) parseArtifactResponse(artifactResponseEl *etree.Eleme // properties are useful in describing which part of the parsing process // failed. However, to discourage inadvertent disclosure the diagnostic // information, the Error() method returns a static string. -func (sp *ServiceProvider) ParseXMLResponse(decodedResponseXML []byte, possibleRequestIDs []string) (*Assertion, error) { +func (sp *ServiceProvider) ParseXMLResponse(decodedResponseXML []byte, possibleRequestIDs []string, currentURL url.URL) (*Assertion, error) { now := TimeNow() var err error retErr := &InvalidResponseError{ @@ -822,7 +962,7 @@ func (sp *ServiceProvider) ParseXMLResponse(decodedResponseXML []byte, possibleR return nil, retErr } - assertion, err := sp.parseResponse(doc.Root(), possibleRequestIDs, now, signatureRequired) + assertion, err := sp.parseResponse(doc.Root(), possibleRequestIDs, now, signatureRequired, currentURL) if err != nil { retErr.PrivateErr = err return nil, retErr @@ -844,7 +984,7 @@ const ( // This function handles decrypting the message, verifying the digital // signature on the assertion, and verifying that the specified conditions // and properties are met. -func (sp *ServiceProvider) parseResponse(responseEl *etree.Element, possibleRequestIDs []string, now time.Time, signatureRequirement signatureRequirement) (*Assertion, error) { +func (sp *ServiceProvider) parseResponse(responseEl *etree.Element, possibleRequestIDs []string, now time.Time, signatureRequirement signatureRequirement, currentURL url.URL) (*Assertion, error) { var responseSignatureErr error var responseHasSignature bool if signatureRequirement == signatureRequired { @@ -867,23 +1007,16 @@ func (sp *ServiceProvider) parseResponse(responseEl *etree.Element, possibleRequ // If the response is *not* signed, the Destination may be omitted. if responseHasSignature || response.Destination != "" { - if response.Destination != sp.AcsURL.String() { - return nil, fmt.Errorf("`Destination` does not match AcsURL (expected %q, actual %q)", sp.AcsURL.String(), response.Destination) + // Per section 3.4.5.2 of the SAML spec, Destination must match the location at which the response was received, i.e. currentURL. + // Historically, we checked against the SP's ACS URL instead of currentURL, which is usually the same but may differ in query params. + // To mitigate the risk of switching to comparing against currentURL, we still allow it if the ACS URL matches, even if the current URL doesn't. + if response.Destination != currentURL.String() && response.Destination != sp.AcsURL.String() { + return nil, fmt.Errorf("`Destination` does not match requested URL or AcsURL (destination %q, requested %q, acs %q)", response.Destination, currentURL.String(), sp.AcsURL.String()) } } - requestIDvalid := false - if sp.AllowIDPInitiated { - requestIDvalid = true - } else { - for _, possibleRequestID := range possibleRequestIDs { - if response.InResponseTo == possibleRequestID { - requestIDvalid = true - } - } - } - if !requestIDvalid { - return nil, fmt.Errorf("`InResponseTo` does not match any of the possible request IDs (expected %v)", possibleRequestIDs) + if err := sp.validateRequestID(response, possibleRequestIDs); err != nil { + return nil, err } if response.IssueInstant.Add(MaxIssueDelay).Before(now) { @@ -959,6 +1092,27 @@ func (sp *ServiceProvider) parseResponse(responseEl *etree.Element, possibleRequ return &assertions[0], nil } +func (sp *ServiceProvider) validateRequestID(response Response, possibleRequestIDs []string) error { + if sp.ValidateRequestID != nil { + return sp.ValidateRequestID(response, possibleRequestIDs) + } + + requestIDvalid := false + if sp.AllowIDPInitiated { + requestIDvalid = true + } else { + for _, possibleRequestID := range possibleRequestIDs { + if response.InResponseTo == possibleRequestID { + requestIDvalid = true + } + } + } + if !requestIDvalid { + return fmt.Errorf("`InResponseTo` does not match any of the possible request IDs (expected %v)", possibleRequestIDs) + } + return nil +} + func (sp *ServiceProvider) parseEncryptedAssertion(encryptedAssertionEl *etree.Element, possibleRequestIDs []string, now time.Time, signatureRequirement signatureRequirement) (*Assertion, error) { assertionEl, err := sp.decryptElement(encryptedAssertionEl) if err != nil { @@ -1076,6 +1230,20 @@ func (sp *ServiceProvider) validateAssertion(assertion *Assertion, possibleReque return fmt.Errorf("assertion Conditions is expired") } + if err := sp.validateAudienceRestriction(assertion); err != nil { + return err + } + return nil +} + +func (sp *ServiceProvider) validateAudienceRestriction(assertion *Assertion) error { + if sp.ValidateAudienceRestriction != nil { + if err := sp.ValidateAudienceRestriction(assertion); err != nil { + return fmt.Errorf("audience restriction validation failed: %w", err) + } + return nil + } + audienceRestrictionsValid := len(assertion.Conditions.AudienceRestrictions) == 0 audience := firstSet(sp.EntityID, sp.MetadataURL.String()) for _, audienceRestriction := range assertion.Conditions.AudienceRestrictions { @@ -1101,9 +1269,28 @@ func (sp *ServiceProvider) validateSignature(el *etree.Element) error { return errSignatureElementNotPresent } - certs, err := sp.getIDPSigningCerts() - if err != nil { - return fmt.Errorf("cannot validate signature on %s: %v", el.Tag, err) + var certs []*x509.Certificate + if sp.IDPMetadata != nil && sp.IDPCertificateFingerprint == nil && sp.IDPCertificateFingerprintAlgorithm == nil && sp.IDPCertificate == nil { + certs, err = sp.getIDPSigningCerts() + if err != nil { + return fmt.Errorf("cannot validate signature on %s: %v", el.Tag, err) + } + } + if sp.IDPMetadata != nil && sp.IDPCertificateFingerprint != nil && sp.IDPCertificateFingerprintAlgorithm != nil && sp.IDPCertificate == nil { + certs, err = sp.getCertBasedOnFingerprint(el) + if err != nil { + return fmt.Errorf("cannot validate signature on %s: %v", el.Tag, err) + } + } + if sp.IDPMetadata != nil && sp.IDPCertificateFingerprint == nil && sp.IDPCertificateFingerprintAlgorithm == nil && sp.IDPCertificate != nil { + cert, err := parseCert(*sp.IDPCertificate) + if err != nil { + return fmt.Errorf("cannot validate signature on %s: %w", el.Tag, err) + } + certs = append(certs, cert) + } + if len(certs) == 0 { + return fmt.Errorf("cannot validate signature on %s: saml config not set up properly, specify either idp metadata url, fingerprints or actual certificate", el.Tag) } certificateStore := dsig.MemoryX509CertificateStore{ @@ -1159,31 +1346,12 @@ func (sp *ServiceProvider) validateSignature(el *etree.Element) error { // SignLogoutRequest adds the `Signature` element to the `LogoutRequest`. func (sp *ServiceProvider) SignLogoutRequest(req *LogoutRequest) error { - keyPair := tls.Certificate{ - Certificate: [][]byte{sp.Certificate.Raw}, - PrivateKey: sp.Key, - Leaf: sp.Certificate, - } - // TODO: add intermediates for SP - // for _, cert := range sp.Intermediates { - // keyPair.Certificate = append(keyPair.Certificate, cert.Raw) - // } - keyStore := dsig.TLSCertKeyStore(keyPair) - - if sp.SignatureMethod != dsig.RSASHA1SignatureMethod && - sp.SignatureMethod != dsig.RSASHA256SignatureMethod && - sp.SignatureMethod != dsig.RSASHA512SignatureMethod { - return fmt.Errorf("invalid signing method %s", sp.SignatureMethod) - } - signatureMethod := sp.SignatureMethod - signingContext := dsig.NewDefaultSigningContext(keyStore) - signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList(canonicalizerPrefixList) - if err := signingContext.SetSignatureMethod(signatureMethod); err != nil { + signingContext, err := GetSigningContext(sp) + if err != nil { return err } assertionEl := req.Element() - signedRequestEl, err := signingContext.SignEnveloped(assertionEl) if err != nil { return err @@ -1213,7 +1381,7 @@ func (sp *ServiceProvider) MakeLogoutRequest(idpURL, nameID string) (*LogoutRequ SPNameQualifier: sp.Metadata().EntityID, }, } - if len(sp.SignatureMethod) > 0 { + if sp.SignatureMethod != "" { if err := sp.SignLogoutRequest(&req); err != nil { return nil, err } @@ -1327,7 +1495,7 @@ func (sp *ServiceProvider) MakeLogoutResponse(idpURL, logoutRequestID string) (* }, } - if len(sp.SignatureMethod) > 0 { + if sp.SignatureMethod != "" { if err := sp.SignLogoutResponse(&response); err != nil { return nil, err } @@ -1424,31 +1592,12 @@ func (r *LogoutResponse) Post(relayState string) []byte { // SignLogoutResponse adds the `Signature` element to the `LogoutResponse`. func (sp *ServiceProvider) SignLogoutResponse(resp *LogoutResponse) error { - keyPair := tls.Certificate{ - Certificate: [][]byte{sp.Certificate.Raw}, - PrivateKey: sp.Key, - Leaf: sp.Certificate, - } - // TODO: add intermediates for SP - // for _, cert := range sp.Intermediates { - // keyPair.Certificate = append(keyPair.Certificate, cert.Raw) - // } - keyStore := dsig.TLSCertKeyStore(keyPair) - - if sp.SignatureMethod != dsig.RSASHA1SignatureMethod && - sp.SignatureMethod != dsig.RSASHA256SignatureMethod && - sp.SignatureMethod != dsig.RSASHA512SignatureMethod { - return fmt.Errorf("invalid signing method %s", sp.SignatureMethod) - } - signatureMethod := sp.SignatureMethod - signingContext := dsig.NewDefaultSigningContext(keyStore) - signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList(canonicalizerPrefixList) - if err := signingContext.SetSignatureMethod(signatureMethod); err != nil { + signingContext, err := GetSigningContext(sp) + if err != nil { return err } assertionEl := resp.Element() - signedRequestEl, err := signingContext.SignEnveloped(assertionEl) if err != nil { return err @@ -1551,7 +1700,7 @@ func (sp *ServiceProvider) ValidateLogoutResponseRedirect(queryParameterData str } doc := etree.NewDocument() - if err := doc.ReadFromBytes(rawResponseBuf); err != nil { + if err := doc.ReadFromBytes(gr); err != nil { retErr.PrivateErr = err return retErr } @@ -1672,7 +1821,11 @@ func elementToBytes(el *etree.Element) ([]byte, error) { doc := etree.NewDocument() doc.SetRoot(el.Copy()) for space, uri := range namespaces { - doc.Root().CreateAttr("xmlns:"+space, uri) + if space == "" { + doc.Root().CreateAttr("xmlns", uri) + } else { + doc.Root().CreateAttr("xmlns:"+space, uri) + } } return doc.WriteToBytes() diff --git a/vendor/github.com/crewjam/saml/xmlenc/cbc.go b/vendor/github.com/crewjam/saml/xmlenc/cbc.go index 991ba1eb..bb0e2882 100644 --- a/vendor/github.com/crewjam/saml/xmlenc/cbc.go +++ b/vendor/github.com/crewjam/saml/xmlenc/cbc.go @@ -3,7 +3,7 @@ package xmlenc import ( "crypto/aes" "crypto/cipher" - "crypto/des" // nolint: gas + "crypto/des" // nolint: gosec "encoding/base64" "errors" "fmt" diff --git a/vendor/github.com/crewjam/saml/xmlenc/decrypt.go b/vendor/github.com/crewjam/saml/xmlenc/decrypt.go index 98a575da..ea288b15 100644 --- a/vendor/github.com/crewjam/saml/xmlenc/decrypt.go +++ b/vendor/github.com/crewjam/saml/xmlenc/decrypt.go @@ -1,8 +1,6 @@ package xmlenc import ( - - // nolint: gas "crypto/rsa" "crypto/x509" "encoding/base64" diff --git a/vendor/github.com/crewjam/saml/xmlenc/digest.go b/vendor/github.com/crewjam/saml/xmlenc/digest.go index 3eaaf7bc..9a46450a 100644 --- a/vendor/github.com/crewjam/saml/xmlenc/digest.go +++ b/vendor/github.com/crewjam/saml/xmlenc/digest.go @@ -6,7 +6,7 @@ import ( "crypto/sha512" "hash" - //nolint:staticcheck // We should support this for legacy reasons. + //nolint:staticcheck,gosec // We should support this for legacy reasons. "golang.org/x/crypto/ripemd160" ) diff --git a/vendor/github.com/crewjam/saml/xmlenc/fuzz.go b/vendor/github.com/crewjam/saml/xmlenc/fuzz.go index c035d65f..ae23bdf3 100644 --- a/vendor/github.com/crewjam/saml/xmlenc/fuzz.go +++ b/vendor/github.com/crewjam/saml/xmlenc/fuzz.go @@ -9,6 +9,7 @@ import ( ) var testKey = func() *rsa.PrivateKey { + //nolint:gosec const keyStr = `-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDkXTUsWzRVpUHjbDpWCfYDfXmQ/q4LkaioZoTpu4ut1Q3eQC5t gD14agJhgT8yzeY5S/YNlwCyuVkjuFyoyTHFX2IOPpz7jnh4KnQ+B1IH9fY/+kmk diff --git a/vendor/github.com/crewjam/saml/xmlenc/pubkey.go b/vendor/github.com/crewjam/saml/xmlenc/pubkey.go index 13d4d9e7..f8eae9cb 100644 --- a/vendor/github.com/crewjam/saml/xmlenc/pubkey.go +++ b/vendor/github.com/crewjam/saml/xmlenc/pubkey.go @@ -125,6 +125,9 @@ func (e RSA) Decrypt(key interface{}, ciphertextEl *etree.Element) ([]byte, erro // the block cipher used is AES-256 CBC and the digest method is SHA-256. You can // specify other ciphers and digest methods by assigning to BlockCipher or // DigestMethod. +// +// OAEP implements the older RSA-OAEP (2001 spec) for backward compatibility, you might +// perfer OAEP_2009_256 over using this method. func OAEP() RSA { return RSA{ BlockCipher: AES256CBC, @@ -139,6 +142,44 @@ func OAEP() RSA { } } +// OAEP_SHA256 returns a version of RSA that implements RSA in OAEP mode. By default +// the block cipher used is AES-256 CBC and the digest method is SHA-256. You can +// specify other ciphers and digest methods by assigning to BlockCipher or +// DigestMethod. +func OAEP_SHA256() RSA { //nolint:revive + return RSA{ + BlockCipher: AES256CBC, + DigestMethod: SHA256, + algorithm: "http://www.w3.org/2009/xmlenc11#rsa-oaep", + + keyEncrypter: func(e RSA, pubKey *rsa.PublicKey, plaintext []byte) ([]byte, error) { + return rsa.EncryptOAEP(e.DigestMethod.Hash(), RandReader, pubKey, plaintext, nil) + }, + keyDecrypter: func(e RSA, privKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error) { + return rsa.DecryptOAEP(e.DigestMethod.Hash(), RandReader, privKey, ciphertext, nil) + }, + } +} + +// OAEP_SHA512 returns a version of RSA that implements RSA in OAEP mode. By default +// the block cipher used is AES-256 CBC and the digest method is SHA-512. You can +// specify other ciphers and digest methods by assigning to BlockCipher or +// DigestMethod. +func OAEP_SHA512() RSA { //nolint:revive + return RSA{ + BlockCipher: AES256CBC, + DigestMethod: SHA512, + algorithm: "http://www.w3.org/2009/xmlenc11#rsa-oaep", + + keyEncrypter: func(e RSA, pubKey *rsa.PublicKey, plaintext []byte) ([]byte, error) { + return rsa.EncryptOAEP(e.DigestMethod.Hash(), RandReader, pubKey, plaintext, nil) + }, + keyDecrypter: func(e RSA, privKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error) { + return rsa.DecryptOAEP(e.DigestMethod.Hash(), RandReader, privKey, ciphertext, nil) + }, + } +} + // PKCS1v15 returns a version of RSA that implements RSA in PKCS1v15 mode. By default // the block cipher used is AES-256 CBC. The DigestMethod field is ignored because PKCS1v15 // does not use a digest function. @@ -147,10 +188,10 @@ func PKCS1v15() RSA { BlockCipher: AES256CBC, DigestMethod: nil, algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5", - keyEncrypter: func(e RSA, pubKey *rsa.PublicKey, plaintext []byte) ([]byte, error) { + keyEncrypter: func(_ RSA, pubKey *rsa.PublicKey, plaintext []byte) ([]byte, error) { return rsa.EncryptPKCS1v15(RandReader, pubKey, plaintext) }, - keyDecrypter: func(e RSA, privKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error) { + keyDecrypter: func(_ RSA, privKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error) { return rsa.DecryptPKCS1v15(RandReader, privKey, ciphertext) }, } diff --git a/vendor/github.com/russellhaering/goxmldsig/canonicalize.go b/vendor/github.com/russellhaering/goxmldsig/canonicalize.go index 75392d13..4d51f4a0 100644 --- a/vendor/github.com/russellhaering/goxmldsig/canonicalize.go +++ b/vendor/github.com/russellhaering/goxmldsig/canonicalize.go @@ -25,8 +25,7 @@ func (c *NullCanonicalizer) Algorithm() AlgorithmID { } func (c *NullCanonicalizer) Canonicalize(el *etree.Element) ([]byte, error) { - scope := make(map[string]struct{}) - return canonicalSerialize(canonicalPrep(el, scope, false, true)) + return canonicalSerialize(canonicalPrep(el, false, true)) } type c14N10ExclusiveCanonicalizer struct { @@ -89,8 +88,7 @@ func MakeC14N11WithCommentsCanonicalizer() Canonicalizer { // Canonicalize transforms the input Element into a serialized XML document in canonical form. func (c *c14N11Canonicalizer) Canonicalize(el *etree.Element) ([]byte, error) { - scope := make(map[string]struct{}) - return canonicalSerialize(canonicalPrep(el, scope, true, c.comments)) + return canonicalSerialize(canonicalPrep(el, true, c.comments)) } func (c *c14N11Canonicalizer) Algorithm() AlgorithmID { @@ -119,9 +117,11 @@ func MakeC14N10WithCommentsCanonicalizer() Canonicalizer { } // Canonicalize transforms the input Element into a serialized XML document in canonical form. -func (c *c14N10RecCanonicalizer) Canonicalize(el *etree.Element) ([]byte, error) { - scope := make(map[string]struct{}) - return canonicalSerialize(canonicalPrep(el, scope, true, c.comments)) +func (c *c14N10RecCanonicalizer) Canonicalize(inputXML *etree.Element) ([]byte, error) { + parentNamespaceAttributes, parentXmlAttributes := getParentNamespaceAndXmlAttributes(inputXML) + inputXMLCopy := inputXML.Copy() + enhanceNamespaceAttributes(inputXMLCopy, parentNamespaceAttributes, parentXmlAttributes) + return canonicalSerialize(canonicalPrep(inputXMLCopy, true, c.comments)) } func (c *c14N10RecCanonicalizer) Algorithm() AlgorithmID { @@ -158,8 +158,12 @@ const nsSpace = "xmlns" // // TODO(russell_h): This is very similar to excCanonicalPrep - perhaps they should // be unified into one parameterized function? -func canonicalPrep(el *etree.Element, seenSoFar map[string]struct{}, strip bool, comments bool) *etree.Element { - _seenSoFar := make(map[string]struct{}) +func canonicalPrep(el *etree.Element, strip bool, comments bool) *etree.Element { + return canonicalPrepInner(el, make(map[string]string), strip, comments) +} + +func canonicalPrepInner(el *etree.Element, seenSoFar map[string]string, strip bool, comments bool) *etree.Element { + _seenSoFar := make(map[string]string) for k, v := range seenSoFar { _seenSoFar[k] = v } @@ -168,16 +172,25 @@ func canonicalPrep(el *etree.Element, seenSoFar map[string]struct{}, strip bool, sort.Sort(etreeutils.SortedAttrs(ne.Attr)) n := 0 for _, attr := range ne.Attr { - if attr.Space != nsSpace { + if attr.Space != nsSpace && !(attr.Space == "" && attr.Key == nsSpace) { ne.Attr[n] = attr n++ continue } - key := attr.Space + ":" + attr.Key - if _, seen := _seenSoFar[key]; !seen { - ne.Attr[n] = attr - n++ - _seenSoFar[key] = struct{}{} + + if attr.Space == nsSpace { + key := attr.Space + ":" + attr.Key + if uri, seen := _seenSoFar[key]; !seen || attr.Value != uri { + ne.Attr[n] = attr + n++ + _seenSoFar[key] = attr.Value + } + } else { + if uri, seen := _seenSoFar[nsSpace]; (!seen && attr.Value != "") || attr.Value != uri { + ne.Attr[n] = attr + n++ + _seenSoFar[nsSpace] = attr.Value + } } } ne.Attr = ne.Attr[:n] @@ -196,7 +209,7 @@ func canonicalPrep(el *etree.Element, seenSoFar map[string]struct{}, strip bool, for i, token := range ne.Child { childElement, ok := token.(*etree.Element) if ok { - ne.Child[i] = canonicalPrep(childElement, _seenSoFar, strip, comments) + ne.Child[i] = canonicalPrepInner(childElement, _seenSoFar, strip, comments) } } @@ -215,3 +228,44 @@ func canonicalSerialize(el *etree.Element) ([]byte, error) { return doc.WriteToBytes() } + +func getParentNamespaceAndXmlAttributes(el *etree.Element) (map[string]string, map[string]string) { + namespaceMap := make(map[string]string, 23) + xmlMap := make(map[string]string, 5) + parents := make([]*etree.Element, 0, 23) + n1 := el.Parent() + if n1 == nil { + return namespaceMap, xmlMap + } + parent := n1 + for parent != nil { + parents = append(parents, parent) + parent = parent.Parent() + } + for i := len(parents) - 1; i > -1; i-- { + elementPos := parents[i] + for _, attr := range elementPos.Attr { + if attr.Space == "xmlns" && (attr.Key != "xml" || attr.Value != "http://www.w3.org/XML/1998/namespace") { + namespaceMap[attr.Key] = attr.Value + } else if attr.Space == "" && attr.Key == "xmlns" { + namespaceMap[attr.Key] = attr.Value + } else if attr.Space == "xml" { + xmlMap[attr.Key] = attr.Value + } + } + } + return namespaceMap, xmlMap +} + +func enhanceNamespaceAttributes(el *etree.Element, parentNamespaces map[string]string, parentXmlAttributes map[string]string) { + for prefix, uri := range parentNamespaces { + if prefix == "xmlns" { + el.CreateAttr("xmlns", uri) + } else { + el.CreateAttr("xmlns:"+prefix, uri) + } + } + for attr, value := range parentXmlAttributes { + el.CreateAttr("xml:"+attr, value) + } +} diff --git a/vendor/github.com/russellhaering/goxmldsig/etreeutils/sort.go b/vendor/github.com/russellhaering/goxmldsig/etreeutils/sort.go index 5871a391..1dc62829 100644 --- a/vendor/github.com/russellhaering/goxmldsig/etreeutils/sort.go +++ b/vendor/github.com/russellhaering/goxmldsig/etreeutils/sort.go @@ -55,12 +55,29 @@ func (a SortedAttrs) Less(i, j int) bool { return false } - // Wow. We're still going. Finally, attributes in the same namespace should be - // sorted by key. Attributes in different namespaces should be sorted by the - // actual namespace (_not_ the prefix). For now just use the prefix. + // Attributes with the same prefix should be sorted by their keys. if a[i].Space == a[j].Space { return a[i].Key < a[j].Key } - return a[i].Space < a[j].Space + // Attributes in the same namespace are sorted by their Namespace URI, not the prefix. + // NOTE: This implementation is not complete because it does not consider namespace + // prefixes declared in ancestor elements. A complete solution would ideally use the + // Attribute.NamespaceURI() method obtain a namespace URI for sorting, but the + // beevik/etree library needs to be fixed to provide the correct value first. + if a[i].Key == a[j].Key { + var leftNS, rightNS etree.Attr + for n := range a { + if a[i].Space == a[n].Key { + leftNS = a[n] + } + if a[j].Space == a[n].Key { + rightNS = a[n] + } + } + // Sort based on the NS URIs + return leftNS.Value < rightNS.Value + } + + return a[i].Key < a[j].Key } diff --git a/vendor/github.com/russellhaering/goxmldsig/validate.go b/vendor/github.com/russellhaering/goxmldsig/validate.go index 45e30138..3d0fc973 100644 --- a/vendor/github.com/russellhaering/goxmldsig/validate.go +++ b/vendor/github.com/russellhaering/goxmldsig/validate.go @@ -363,17 +363,17 @@ func (ctx *ValidationContext) findSignature(root *etree.Element) (*types.Signatu canonicalSignedInfo = detachedSignedInfo case CanonicalXML11AlgorithmId, CanonicalXML10RecAlgorithmId: - canonicalSignedInfo = canonicalPrep(detachedSignedInfo, map[string]struct{}{}, true, false) + canonicalSignedInfo = canonicalPrep(detachedSignedInfo, true, false) case CanonicalXML11WithCommentsAlgorithmId, CanonicalXML10WithCommentsAlgorithmId: - canonicalSignedInfo = canonicalPrep(detachedSignedInfo, map[string]struct{}{}, true, true) + canonicalSignedInfo = canonicalPrep(detachedSignedInfo, true, true) default: return fmt.Errorf("invalid CanonicalizationMethod on Signature: %s", c14NAlgorithm) } + signatureEl.InsertChildAt(signedInfo.Index(), canonicalSignedInfo) signatureEl.RemoveChild(signedInfo) - signatureEl.AddChild(canonicalSignedInfo) found = true diff --git a/vendor/modules.txt b/vendor/modules.txt index 4a05d044..58d97c72 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -23,8 +23,8 @@ git.autistici.org/id/usermetadb # github.com/NYTimes/gziphandler v1.1.1 ## explicit; go 1.11 github.com/NYTimes/gziphandler -# github.com/beevik/etree v1.1.0 -## explicit +# github.com/beevik/etree v1.5.0 +## explicit; go 1.21.0 github.com/beevik/etree # github.com/beorn7/perks v1.0.1 ## explicit; go 1.11 @@ -38,8 +38,8 @@ github.com/cespare/xxhash/v2 # github.com/coreos/go-systemd/v22 v22.5.0 ## explicit; go 1.12 github.com/coreos/go-systemd/v22/daemon -# github.com/crewjam/saml v0.4.14 -## explicit; go 1.19 +# github.com/crewjam/saml v0.5.0 +## explicit; go 1.22 github.com/crewjam/saml github.com/crewjam/saml/logger github.com/crewjam/saml/xmlenc @@ -145,7 +145,7 @@ github.com/prometheus/procfs/internal/util ## explicit; go 1.13 github.com/rs/cors github.com/rs/cors/internal -# github.com/russellhaering/goxmldsig v1.3.0 +# github.com/russellhaering/goxmldsig v1.4.0 ## explicit; go 1.15 github.com/russellhaering/goxmldsig github.com/russellhaering/goxmldsig/etreeutils -- GitLab