diff --git a/Dockerfile b/Dockerfile index cfdc7a73..edb985e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ COPY ./gui /go/src/github.com/documize/community/gui RUN npm --network-timeout=100000 install RUN npm run build -- --environment=production --output-path dist-prod --suppress-sizes true -FROM golang:1.19-alpine as builder +FROM golang:1.21-alpine as builder WORKDIR /go/src/github.com/documize/community COPY . /go/src/github.com/documize/community COPY --from=frontbuilder /go/src/github.com/documize/community/gui/dist-prod/assets /go/src/github.com/documize/community/edition/static/public/assets diff --git a/edition/community.go b/edition/community.go index a4eeda44..776fff07 100644 --- a/edition/community.go +++ b/edition/community.go @@ -40,9 +40,9 @@ func main() { // Specify the product edition. rt.Product = domain.Product{} rt.Product.Major = "5" - rt.Product.Minor = "10" + rt.Product.Minor = "11" rt.Product.Patch = "0" - rt.Product.Revision = "1695054225" + rt.Product.Revision = "1704915883" rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch) rt.Product.Edition = domain.CommunityEdition rt.Product.Title = "Community" diff --git a/gui/package.json b/gui/package.json index b8365cac..89797f23 100644 --- a/gui/package.json +++ b/gui/package.json @@ -1,6 +1,6 @@ { "name": "documize", - "version": "5.10.0", + "version": "5.11.0", "private": true, "description": "Documize Community", "repository": "", diff --git a/gui/public/manifest.json b/gui/public/manifest.json index 4fb79e00..956c7e88 100644 --- a/gui/public/manifest.json +++ b/gui/public/manifest.json @@ -6,21 +6,5 @@ "theme_color": "#280A42", "description": "Organized documents", "start_url": "/?utm_source=homescreen", - "icons": [{ - "src": "assets/img/icon.png", - "sizes": "64x64", - "type": "image/png" - }, { - "src": "assets/img/icon.png", - "sizes": "128x128", - "type": "image/png" - }, { - "src": "assets/img/icon.png", - "sizes": "256x256", - "type": "image/png" - }, { - "src": "assets/img/icon.png", - "sizes": "1024x1024", - "type": "image/png" - }] + "icons": [] } diff --git a/vendor/github.com/Azure/go-ntlmssp/SECURITY.md b/vendor/github.com/Azure/go-ntlmssp/SECURITY.md new file mode 100644 index 00000000..e138ec5d --- /dev/null +++ b/vendor/github.com/Azure/go-ntlmssp/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). + + diff --git a/vendor/github.com/Azure/go-ntlmssp/authenticate_message.go b/vendor/github.com/Azure/go-ntlmssp/authenticate_message.go index c8930680..ab183db6 100644 --- a/vendor/github.com/Azure/go-ntlmssp/authenticate_message.go +++ b/vendor/github.com/Azure/go-ntlmssp/authenticate_message.go @@ -42,7 +42,7 @@ func (m authenicateMessage) MarshalBinary() ([]byte, error) { } target, user := toUnicode(m.TargetName), toUnicode(m.UserName) - workstation := toUnicode("go-ntlmssp") + workstation := toUnicode("") ptr := binary.Size(&authenticateMessageFields{}) f := authenticateMessageFields{ @@ -82,7 +82,7 @@ func (m authenicateMessage) MarshalBinary() ([]byte, error) { //ProcessChallenge crafts an AUTHENTICATE message in response to the CHALLENGE message //that was received from the server -func ProcessChallenge(challengeMessageData []byte, user, password string) ([]byte, error) { +func ProcessChallenge(challengeMessageData []byte, user, password string, domainNeeded bool) ([]byte, error) { if user == "" && password == "" { return nil, errors.New("Anonymous authentication not supported") } @@ -98,6 +98,10 @@ func ProcessChallenge(challengeMessageData []byte, user, password string) ([]byt if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEKEYEXCH) { return nil, errors.New("Key exchange requested but not supported (NTLMSSP_NEGOTIATE_KEY_EXCH)") } + + if !domainNeeded { + cm.TargetName = "" + } am := authenicateMessage{ UserName: user, diff --git a/vendor/github.com/Azure/go-ntlmssp/authheader.go b/vendor/github.com/Azure/go-ntlmssp/authheader.go index aac3f77d..c9d30d32 100644 --- a/vendor/github.com/Azure/go-ntlmssp/authheader.go +++ b/vendor/github.com/Azure/go-ntlmssp/authheader.go @@ -5,26 +5,55 @@ import ( "strings" ) -type authheader string +type authheader []string func (h authheader) IsBasic() bool { - return strings.HasPrefix(string(h), "Basic ") + for _, s := range h { + if strings.HasPrefix(string(s), "Basic ") { + return true + } + } + return false +} + +func (h authheader) Basic() string { + for _, s := range h { + if strings.HasPrefix(string(s), "Basic ") { + return s + } + } + return "" } func (h authheader) IsNegotiate() bool { - return strings.HasPrefix(string(h), "Negotiate") + for _, s := range h { + if strings.HasPrefix(string(s), "Negotiate") { + return true + } + } + return false } func (h authheader) IsNTLM() bool { - return strings.HasPrefix(string(h), "NTLM") + for _, s := range h { + if strings.HasPrefix(string(s), "NTLM") { + return true + } + } + return false } func (h authheader) GetData() ([]byte, error) { - p := strings.Split(string(h), " ") - if len(p) < 2 { - return nil, nil + for _, s := range h { + if strings.HasPrefix(string(s), "NTLM") || strings.HasPrefix(string(s), "Negotiate") || strings.HasPrefix(string(s), "Basic ") { + p := strings.Split(string(s), " ") + if len(p) < 2 { + return nil, nil + } + return base64.StdEncoding.DecodeString(string(p[1])) + } } - return base64.StdEncoding.DecodeString(string(p[1])) + return nil, nil } func (h authheader) GetBasicCreds() (username, password string, err error) { diff --git a/vendor/github.com/Azure/go-ntlmssp/negotiator.go b/vendor/github.com/Azure/go-ntlmssp/negotiator.go index 7705eae4..cce4955d 100644 --- a/vendor/github.com/Azure/go-ntlmssp/negotiator.go +++ b/vendor/github.com/Azure/go-ntlmssp/negotiator.go @@ -10,15 +10,22 @@ import ( ) // GetDomain : parse domain name from based on slashes in the input -func GetDomain(user string) (string, string) { +// Need to check for upn as well +func GetDomain(user string) (string, string, bool) { domain := "" + domainNeeded := false if strings.Contains(user, "\\") { ucomponents := strings.SplitN(user, "\\", 2) domain = ucomponents[0] user = ucomponents[1] + domainNeeded = true + } else if strings.Contains(user, "@") { + domainNeeded = false + } else { + domainNeeded = true } - return user, domain + return user, domain, domainNeeded } //Negotiator is a http.Roundtripper decorator that automatically @@ -34,10 +41,11 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) rt = http.DefaultTransport } // If it is not basic auth, just round trip the request as usual - reqauth := authheader(req.Header.Get("Authorization")) + reqauth := authheader(req.Header.Values("Authorization")) if !reqauth.IsBasic() { return rt.RoundTrip(req) } + reqauthBasic := reqauth.Basic() // Save request body body := bytes.Buffer{} if req.Body != nil { @@ -59,11 +67,10 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) if res.StatusCode != http.StatusUnauthorized { return res, err } - - resauth := authheader(res.Header.Get("Www-Authenticate")) + resauth := authheader(res.Header.Values("Www-Authenticate")) if !resauth.IsNegotiate() && !resauth.IsNTLM() { // Unauthorized, Negotiate not requested, let's try with basic auth - req.Header.Set("Authorization", string(reqauth)) + req.Header.Set("Authorization", string(reqauthBasic)) io.Copy(ioutil.Discard, res.Body) res.Body.Close() req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes())) @@ -75,7 +82,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) if res.StatusCode != http.StatusUnauthorized { return res, err } - resauth = authheader(res.Header.Get("Www-Authenticate")) + resauth = authheader(res.Header.Values("Www-Authenticate")) } if resauth.IsNegotiate() || resauth.IsNTLM() { @@ -91,7 +98,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) // get domain from username domain := "" - u, domain = GetDomain(u) + u, domain, domainNeeded := GetDomain(u) // send negotiate negotiateMessage, err := NewNegotiateMessage(domain, "") @@ -112,7 +119,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) } // receive challenge? - resauth = authheader(res.Header.Get("Www-Authenticate")) + resauth = authheader(res.Header.Values("Www-Authenticate")) challengeMessage, err := resauth.GetData() if err != nil { return nil, err @@ -125,7 +132,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) res.Body.Close() // send authenticate - authenticateMessage, err := ProcessChallenge(challengeMessage, u, p) + authenticateMessage, err := ProcessChallenge(challengeMessage, u, p, domainNeeded) if err != nil { return nil, err } diff --git a/vendor/github.com/BurntSushi/toml/.gitignore b/vendor/github.com/BurntSushi/toml/.gitignore index 0cd38003..fe79e3ad 100644 --- a/vendor/github.com/BurntSushi/toml/.gitignore +++ b/vendor/github.com/BurntSushi/toml/.gitignore @@ -1,5 +1,2 @@ -TAGS -tags -.*.swp -tomlcheck/tomlcheck -toml.test +/toml.test +/toml-test diff --git a/vendor/github.com/BurntSushi/toml/.travis.yml b/vendor/github.com/BurntSushi/toml/.travis.yml deleted file mode 100644 index 8b8afc4f..00000000 --- a/vendor/github.com/BurntSushi/toml/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: go -go: - - 1.1 - - 1.2 - - 1.3 - - 1.4 - - 1.5 - - 1.6 - - tip -install: - - go install ./... - - go get github.com/BurntSushi/toml-test -script: - - export PATH="$PATH:$HOME/gopath/bin" - - make test diff --git a/vendor/github.com/BurntSushi/toml/COMPATIBLE b/vendor/github.com/BurntSushi/toml/COMPATIBLE deleted file mode 100644 index 6efcfd0c..00000000 --- a/vendor/github.com/BurntSushi/toml/COMPATIBLE +++ /dev/null @@ -1,3 +0,0 @@ -Compatible with TOML version -[v0.4.0](https://github.com/toml-lang/toml/blob/v0.4.0/versions/en/toml-v0.4.0.md) - diff --git a/vendor/github.com/BurntSushi/toml/Makefile b/vendor/github.com/BurntSushi/toml/Makefile deleted file mode 100644 index 3600848d..00000000 --- a/vendor/github.com/BurntSushi/toml/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -install: - go install ./... - -test: install - go test -v - toml-test toml-test-decoder - toml-test -encoder toml-test-encoder - -fmt: - gofmt -w *.go */*.go - colcheck *.go */*.go - -tags: - find ./ -name '*.go' -print0 | xargs -0 gotags > TAGS - -push: - git push origin master - git push github master - diff --git a/vendor/github.com/BurntSushi/toml/README.md b/vendor/github.com/BurntSushi/toml/README.md index 7c1b37ec..3651cfa9 100644 --- a/vendor/github.com/BurntSushi/toml/README.md +++ b/vendor/github.com/BurntSushi/toml/README.md @@ -1,46 +1,26 @@ -## TOML parser and encoder for Go with reflection - TOML stands for Tom's Obvious, Minimal Language. This Go package provides a -reflection interface similar to Go's standard library `json` and `xml` -packages. This package also supports the `encoding.TextUnmarshaler` and -`encoding.TextMarshaler` interfaces so that you can define custom data -representations. (There is an example of this below.) +reflection interface similar to Go's standard library `json` and `xml` packages. -Spec: https://github.com/toml-lang/toml +Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0). -Compatible with TOML version -[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md) +Documentation: https://godocs.io/github.com/BurntSushi/toml -Documentation: https://godoc.org/github.com/BurntSushi/toml +See the [releases page](https://github.com/BurntSushi/toml/releases) for a +changelog; this information is also in the git tag annotations (e.g. `git show +v0.4.0`). -Installation: +This library requires Go 1.13 or newer; add it to your go.mod with: -```bash -go get github.com/BurntSushi/toml -``` + % go get github.com/BurntSushi/toml@latest -Try the toml validator: +It also comes with a TOML validator CLI tool: -```bash -go get github.com/BurntSushi/toml/cmd/tomlv -tomlv some-toml-file.toml -``` - -[![Build Status](https://travis-ci.org/BurntSushi/toml.svg?branch=master)](https://travis-ci.org/BurntSushi/toml) [![GoDoc](https://godoc.org/github.com/BurntSushi/toml?status.svg)](https://godoc.org/github.com/BurntSushi/toml) - -### Testing - -This package passes all tests in -[toml-test](https://github.com/BurntSushi/toml-test) for both the decoder -and the encoder. + % go install github.com/BurntSushi/toml/cmd/tomlv@latest + % tomlv some-toml-file.toml ### Examples - -This package works similarly to how the Go standard library handles `XML` -and `JSON`. Namely, data is loaded into Go values via reflection. - -For the simplest example, consider some TOML file as just a list of keys -and values: +For the simplest example, consider some TOML file as just a list of keys and +values: ```toml Age = 25 @@ -50,29 +30,23 @@ Perfection = [ 6, 28, 496, 8128 ] DOB = 1987-07-05T05:45:00Z ``` -Which could be defined in Go as: +Which can be decoded with: ```go type Config struct { - Age int - Cats []string - Pi float64 - Perfection []int - DOB time.Time // requires `import time` + Age int + Cats []string + Pi float64 + Perfection []int + DOB time.Time } -``` -And then decoded with: - -```go var conf Config -if _, err := toml.Decode(tomlData, &conf); err != nil { - // handle error -} +_, err := toml.Decode(tomlData, &conf) ``` -You can also use struct tags if your struct field name doesn't map to a TOML -key value directly: +You can also use struct tags if your struct field name doesn't map to a TOML key +value directly: ```toml some_key_NAME = "wat" @@ -80,139 +54,67 @@ some_key_NAME = "wat" ```go type TOML struct { - ObscureKey string `toml:"some_key_NAME"` + ObscureKey string `toml:"some_key_NAME"` } ``` -### Using the `encoding.TextUnmarshaler` interface +Beware that like other decoders **only exported fields** are considered when +encoding and decoding; private fields are silently ignored. -Here's an example that automatically parses duration strings into -`time.Duration` values: +### Using the `Marshaler` and `encoding.TextUnmarshaler` interfaces +Here's an example that automatically parses values in a `mail.Address`: ```toml -[[song]] -name = "Thunder Road" -duration = "4m49s" - -[[song]] -name = "Stairway to Heaven" -duration = "8m03s" -``` - -Which can be decoded with: - -```go -type song struct { - Name string - Duration duration -} -type songs struct { - Song []song -} -var favorites songs -if _, err := toml.Decode(blob, &favorites); err != nil { - log.Fatal(err) -} - -for _, s := range favorites.Song { - fmt.Printf("%s (%s)\n", s.Name, s.Duration) -} -``` - -And you'll also need a `duration` type that satisfies the -`encoding.TextUnmarshaler` interface: - -```go -type duration struct { - time.Duration -} - -func (d *duration) UnmarshalText(text []byte) error { - var err error - d.Duration, err = time.ParseDuration(string(text)) - return err -} -``` - -### More complex usage - -Here's an example of how to load the example from the official spec page: - -```toml -# This is a TOML document. Boom. - -title = "TOML Example" - -[owner] -name = "Tom Preston-Werner" -organization = "GitHub" -bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." -dob = 1979-05-27T07:32:00Z # First class dates? Why not? - -[database] -server = "192.168.1.1" -ports = [ 8001, 8001, 8002 ] -connection_max = 5000 -enabled = true - -[servers] - - # You can indent as you please. Tabs or spaces. TOML don't care. - [servers.alpha] - ip = "10.0.0.1" - dc = "eqdc10" - - [servers.beta] - ip = "10.0.0.2" - dc = "eqdc10" - -[clients] -data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it - -# Line breaks are OK when inside arrays -hosts = [ - "alpha", - "omega" +contacts = [ + "Donald Duck ", + "Scrooge McDuck ", ] ``` -And the corresponding Go types are: +Can be decoded with: ```go -type tomlConfig struct { - Title string - Owner ownerInfo - DB database `toml:"database"` - Servers map[string]server - Clients clients +// Create address type which satisfies the encoding.TextUnmarshaler interface. +type address struct { + *mail.Address } -type ownerInfo struct { - Name string - Org string `toml:"organization"` - Bio string - DOB time.Time +func (a *address) UnmarshalText(text []byte) error { + var err error + a.Address, err = mail.ParseAddress(string(text)) + return err } -type database struct { - Server string - Ports []int - ConnMax int `toml:"connection_max"` - Enabled bool -} +// Decode it. +func decode() { + blob := ` + contacts = [ + "Donald Duck ", + "Scrooge McDuck ", + ] + ` -type server struct { - IP string - DC string -} + var contacts struct { + Contacts []address + } -type clients struct { - Data [][]interface{} - Hosts []string + _, err := toml.Decode(blob, &contacts) + if err != nil { + log.Fatal(err) + } + + for _, c := range contacts.Contacts { + fmt.Printf("%#v\n", c.Address) + } + + // Output: + // &mail.Address{Name:"Donald Duck", Address:"donald@duckburg.com"} + // &mail.Address{Name:"Scrooge McDuck", Address:"scrooge@duckburg.com"} } ``` -Note that a case insensitive match will be tried if an exact match can't be -found. +To target TOML specifically you can implement `UnmarshalTOML` TOML interface in +a similar way. -A working example of the above can be found in `_examples/example.{go,toml}`. +### More complex usage +See the [`_example/`](/_example) directory for a more complex example. diff --git a/vendor/github.com/BurntSushi/toml/decode.go b/vendor/github.com/BurntSushi/toml/decode.go index b0fd51d5..4d38f3bf 100644 --- a/vendor/github.com/BurntSushi/toml/decode.go +++ b/vendor/github.com/BurntSushi/toml/decode.go @@ -1,157 +1,199 @@ package toml import ( + "bytes" + "encoding" + "encoding/json" "fmt" "io" "io/ioutil" "math" + "os" "reflect" + "strconv" "strings" "time" ) -func e(format string, args ...interface{}) error { - return fmt.Errorf("toml: "+format, args...) -} - // Unmarshaler is the interface implemented by objects that can unmarshal a // TOML description of themselves. type Unmarshaler interface { UnmarshalTOML(interface{}) error } -// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`. -func Unmarshal(p []byte, v interface{}) error { - _, err := Decode(string(p), v) +// Unmarshal decodes the contents of data in TOML format into a pointer v. +// +// See [Decoder] for a description of the decoding process. +func Unmarshal(data []byte, v interface{}) error { + _, err := NewDecoder(bytes.NewReader(data)).Decode(v) return err } +// Decode the TOML data in to the pointer v. +// +// See [Decoder] for a description of the decoding process. +func Decode(data string, v interface{}) (MetaData, error) { + return NewDecoder(strings.NewReader(data)).Decode(v) +} + +// DecodeFile reads the contents of a file and decodes it with [Decode]. +func DecodeFile(path string, v interface{}) (MetaData, error) { + fp, err := os.Open(path) + if err != nil { + return MetaData{}, err + } + defer fp.Close() + return NewDecoder(fp).Decode(v) +} + // Primitive is a TOML value that hasn't been decoded into a Go value. -// When using the various `Decode*` functions, the type `Primitive` may -// be given to any value, and its decoding will be delayed. // -// A `Primitive` value can be decoded using the `PrimitiveDecode` function. +// This type can be used for any value, which will cause decoding to be delayed. +// You can use [PrimitiveDecode] to "manually" decode these values. // -// The underlying representation of a `Primitive` value is subject to change. -// Do not rely on it. +// NOTE: The underlying representation of a `Primitive` value is subject to +// change. Do not rely on it. // -// N.B. Primitive values are still parsed, so using them will only avoid -// the overhead of reflection. They can be useful when you don't know the -// exact type of TOML data until run time. +// NOTE: Primitive values are still parsed, so using them will only avoid the +// overhead of reflection. They can be useful when you don't know the exact type +// of TOML data until runtime. type Primitive struct { undecoded interface{} context Key } -// DEPRECATED! +// The significand precision for float32 and float64 is 24 and 53 bits; this is +// the range a natural number can be stored in a float without loss of data. +const ( + maxSafeFloat32Int = 16777215 // 2^24-1 + maxSafeFloat64Int = int64(9007199254740991) // 2^53-1 +) + +// Decoder decodes TOML data. // -// Use MetaData.PrimitiveDecode instead. -func PrimitiveDecode(primValue Primitive, v interface{}) error { - md := MetaData{decoded: make(map[string]bool)} - return md.unify(primValue.undecoded, rvalue(v)) +// TOML tables correspond to Go structs or maps; they can be used +// interchangeably, but structs offer better type safety. +// +// TOML table arrays correspond to either a slice of structs or a slice of maps. +// +// TOML datetimes correspond to [time.Time]. Local datetimes are parsed in the +// local timezone. +// +// [time.Duration] types are treated as nanoseconds if the TOML value is an +// integer, or they're parsed with time.ParseDuration() if they're strings. +// +// All other TOML types (float, string, int, bool and array) correspond to the +// obvious Go types. +// +// An exception to the above rules is if a type implements the TextUnmarshaler +// interface, in which case any primitive TOML value (floats, strings, integers, +// booleans, datetimes) will be converted to a []byte and given to the value's +// UnmarshalText method. See the Unmarshaler example for a demonstration with +// email addresses. +// +// # Key mapping +// +// TOML keys can map to either keys in a Go map or field names in a Go struct. +// The special `toml` struct tag can be used to map TOML keys to struct fields +// that don't match the key name exactly (see the example). A case insensitive +// match to struct names will be tried if an exact match can't be found. +// +// The mapping between TOML values and Go values is loose. That is, there may +// exist TOML values that cannot be placed into your representation, and there +// may be parts of your representation that do not correspond to TOML values. +// This loose mapping can be made stricter by using the IsDefined and/or +// Undecoded methods on the MetaData returned. +// +// This decoder does not handle cyclic types. Decode will not terminate if a +// cyclic type is passed. +type Decoder struct { + r io.Reader } -// PrimitiveDecode is just like the other `Decode*` functions, except it -// decodes a TOML value that has already been parsed. Valid primitive values -// can *only* be obtained from values filled by the decoder functions, -// including this method. (i.e., `v` may contain more `Primitive` -// values.) +// NewDecoder creates a new Decoder. +func NewDecoder(r io.Reader) *Decoder { + return &Decoder{r: r} +} + +var ( + unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem() + unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + primitiveType = reflect.TypeOf((*Primitive)(nil)).Elem() +) + +// Decode TOML data in to the pointer `v`. +func (dec *Decoder) Decode(v interface{}) (MetaData, error) { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr { + s := "%q" + if reflect.TypeOf(v) == nil { + s = "%v" + } + + return MetaData{}, fmt.Errorf("toml: cannot decode to non-pointer "+s, reflect.TypeOf(v)) + } + if rv.IsNil() { + return MetaData{}, fmt.Errorf("toml: cannot decode to nil value of %q", reflect.TypeOf(v)) + } + + // Check if this is a supported type: struct, map, interface{}, or something + // that implements UnmarshalTOML or UnmarshalText. + rv = indirect(rv) + rt := rv.Type() + if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map && + !(rv.Kind() == reflect.Interface && rv.NumMethod() == 0) && + !rt.Implements(unmarshalToml) && !rt.Implements(unmarshalText) { + return MetaData{}, fmt.Errorf("toml: cannot decode to type %s", rt) + } + + // TODO: parser should read from io.Reader? Or at the very least, make it + // read from []byte rather than string + data, err := ioutil.ReadAll(dec.r) + if err != nil { + return MetaData{}, err + } + + p, err := parse(string(data)) + if err != nil { + return MetaData{}, err + } + + md := MetaData{ + mapping: p.mapping, + keyInfo: p.keyInfo, + keys: p.ordered, + decoded: make(map[string]struct{}, len(p.ordered)), + context: nil, + data: data, + } + return md, md.unify(p.mapping, rv) +} + +// PrimitiveDecode is just like the other Decode* functions, except it decodes a +// TOML value that has already been parsed. Valid primitive values can *only* be +// obtained from values filled by the decoder functions, including this method. +// (i.e., v may contain more [Primitive] values.) // -// Meta data for primitive values is included in the meta data returned by -// the `Decode*` functions with one exception: keys returned by the Undecoded -// method will only reflect keys that were decoded. Namely, any keys hidden -// behind a Primitive will be considered undecoded. Executing this method will -// update the undecoded keys in the meta data. (See the example.) +// Meta data for primitive values is included in the meta data returned by the +// Decode* functions with one exception: keys returned by the Undecoded method +// will only reflect keys that were decoded. Namely, any keys hidden behind a +// Primitive will be considered undecoded. Executing this method will update the +// undecoded keys in the meta data. (See the example.) func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error { md.context = primValue.context defer func() { md.context = nil }() return md.unify(primValue.undecoded, rvalue(v)) } -// Decode will decode the contents of `data` in TOML format into a pointer -// `v`. -// -// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be -// used interchangeably.) -// -// TOML arrays of tables correspond to either a slice of structs or a slice -// of maps. -// -// TOML datetimes correspond to Go `time.Time` values. -// -// All other TOML types (float, string, int, bool and array) correspond -// to the obvious Go types. -// -// An exception to the above rules is if a type implements the -// encoding.TextUnmarshaler interface. In this case, any primitive TOML value -// (floats, strings, integers, booleans and datetimes) will be converted to -// a byte string and given to the value's UnmarshalText method. See the -// Unmarshaler example for a demonstration with time duration strings. -// -// Key mapping -// -// TOML keys can map to either keys in a Go map or field names in a Go -// struct. The special `toml` struct tag may be used to map TOML keys to -// struct fields that don't match the key name exactly. (See the example.) -// A case insensitive match to struct names will be tried if an exact match -// can't be found. -// -// The mapping between TOML values and Go values is loose. That is, there -// may exist TOML values that cannot be placed into your representation, and -// there may be parts of your representation that do not correspond to -// TOML values. This loose mapping can be made stricter by using the IsDefined -// and/or Undecoded methods on the MetaData returned. -// -// This decoder will not handle cyclic types. If a cyclic type is passed, -// `Decode` will not terminate. -func Decode(data string, v interface{}) (MetaData, error) { - rv := reflect.ValueOf(v) - if rv.Kind() != reflect.Ptr { - return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v)) - } - if rv.IsNil() { - return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v)) - } - p, err := parse(data) - if err != nil { - return MetaData{}, err - } - md := MetaData{ - p.mapping, p.types, p.ordered, - make(map[string]bool, len(p.ordered)), nil, - } - return md, md.unify(p.mapping, indirect(rv)) -} - -// DecodeFile is just like Decode, except it will automatically read the -// contents of the file at `fpath` and decode it for you. -func DecodeFile(fpath string, v interface{}) (MetaData, error) { - bs, err := ioutil.ReadFile(fpath) - if err != nil { - return MetaData{}, err - } - return Decode(string(bs), v) -} - -// DecodeReader is just like Decode, except it will consume all bytes -// from the reader and decode it for you. -func DecodeReader(r io.Reader, v interface{}) (MetaData, error) { - bs, err := ioutil.ReadAll(r) - if err != nil { - return MetaData{}, err - } - return Decode(string(bs), v) -} - // unify performs a sort of type unification based on the structure of `rv`, // which is the client representation. // // Any type mismatch produces an error. Finding a type that we don't know // how to handle produces an unsupported type error. func (md *MetaData) unify(data interface{}, rv reflect.Value) error { - // Special case. Look for a `Primitive` value. - if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() { + // TODO: #76 would make this superfluous after implemented. + if rv.Type() == primitiveType { // Save the undecoded data and the key context into the primitive // value. context := make(Key, len(md.context)) @@ -163,36 +205,24 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error { return nil } - // Special case. Unmarshaler Interface support. - if rv.CanAddr() { - if v, ok := rv.Addr().Interface().(Unmarshaler); ok { - return v.UnmarshalTOML(data) - } + rvi := rv.Interface() + if v, ok := rvi.(Unmarshaler); ok { + return v.UnmarshalTOML(data) } - - // Special case. Handle time.Time values specifically. - // TODO: Remove this code when we decide to drop support for Go 1.1. - // This isn't necessary in Go 1.2 because time.Time satisfies the encoding - // interfaces. - if rv.Type().AssignableTo(rvalue(time.Time{}).Type()) { - return md.unifyDatetime(data, rv) - } - - // Special case. Look for a value satisfying the TextUnmarshaler interface. - if v, ok := rv.Interface().(TextUnmarshaler); ok { + if v, ok := rvi.(encoding.TextUnmarshaler); ok { return md.unifyText(data, v) } - // BUG(burntsushi) + + // TODO: // The behavior here is incorrect whenever a Go type satisfies the - // encoding.TextUnmarshaler interface but also corresponds to a TOML - // hash or array. In particular, the unmarshaler should only be applied - // to primitive TOML values. But at this point, it will be applied to - // all kinds of values and produce an incorrect error whenever those values - // are hashes or arrays (including arrays of tables). + // encoding.TextUnmarshaler interface but also corresponds to a TOML hash or + // array. In particular, the unmarshaler should only be applied to primitive + // TOML values. But at this point, it will be applied to all kinds of values + // and produce an incorrect error whenever those values are hashes or arrays + // (including arrays of tables). k := rv.Kind() - // laziness if k >= reflect.Int && k <= reflect.Uint64 { return md.unifyInt(data, rv) } @@ -218,17 +248,14 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error { case reflect.Bool: return md.unifyBool(data, rv) case reflect.Interface: - // we only support empty interfaces. - if rv.NumMethod() > 0 { - return e("unsupported type %s", rv.Type()) + if rv.NumMethod() > 0 { /// Only empty interfaces are supported. + return md.e("unsupported type %s", rv.Type()) } return md.unifyAnything(data, rv) - case reflect.Float32: - fallthrough - case reflect.Float64: + case reflect.Float32, reflect.Float64: return md.unifyFloat64(data, rv) } - return e("unsupported type %s", rv.Kind()) + return md.e("unsupported type %s", rv.Kind()) } func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error { @@ -237,7 +264,7 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error { if mapping == nil { return nil } - return e("type mismatch for %s: expected table but found %T", + return md.e("type mismatch for %s: expected table but found %T", rv.Type().String(), mapping) } @@ -259,17 +286,18 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error { for _, i := range f.index { subv = indirect(subv.Field(i)) } + if isUnifiable(subv) { - md.decoded[md.context.add(key).String()] = true + md.decoded[md.context.add(key).String()] = struct{}{} md.context = append(md.context, key) - if err := md.unify(datum, subv); err != nil { + + err := md.unify(datum, subv) + if err != nil { return err } md.context = md.context[0 : len(md.context)-1] } else if f.name != "" { - // Bad user! No soup for you! - return e("cannot write unexported field %s.%s", - rv.Type().String(), f.name) + return md.e("cannot write unexported field %s.%s", rv.Type().String(), f.name) } } } @@ -277,28 +305,43 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error { } func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error { + keyType := rv.Type().Key().Kind() + if keyType != reflect.String && keyType != reflect.Interface { + return fmt.Errorf("toml: cannot decode to a map with non-string key type (%s in %q)", + keyType, rv.Type()) + } + tmap, ok := mapping.(map[string]interface{}) if !ok { if tmap == nil { return nil } - return badtype("map", mapping) + return md.badtype("map", mapping) } if rv.IsNil() { rv.Set(reflect.MakeMap(rv.Type())) } for k, v := range tmap { - md.decoded[md.context.add(k).String()] = true + md.decoded[md.context.add(k).String()] = struct{}{} md.context = append(md.context, k) - rvkey := indirect(reflect.New(rv.Type().Key())) rvval := reflect.Indirect(reflect.New(rv.Type().Elem())) - if err := md.unify(v, rvval); err != nil { + + err := md.unify(v, indirect(rvval)) + if err != nil { return err } md.context = md.context[0 : len(md.context)-1] - rvkey.SetString(k) + rvkey := indirect(reflect.New(rv.Type().Key())) + + switch keyType { + case reflect.Interface: + rvkey.Set(reflect.ValueOf(k)) + case reflect.String: + rvkey.SetString(k) + } + rv.SetMapIndex(rvkey, rvval) } return nil @@ -310,12 +353,10 @@ func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error { if !datav.IsValid() { return nil } - return badtype("slice", data) + return md.badtype("slice", data) } - sliceLen := datav.Len() - if sliceLen != rv.Len() { - return e("expected array length %d; got TOML array of length %d", - rv.Len(), sliceLen) + if l := datav.Len(); l != rv.Len() { + return md.e("expected array length %d; got TOML array of length %d", rv.Len(), l) } return md.unifySliceArray(datav, rv) } @@ -326,7 +367,7 @@ func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error { if !datav.IsValid() { return nil } - return badtype("slice", data) + return md.badtype("slice", data) } n := datav.Len() if rv.IsNil() || rv.Cap() < n { @@ -337,37 +378,45 @@ func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error { } func (md *MetaData) unifySliceArray(data, rv reflect.Value) error { - sliceLen := data.Len() - for i := 0; i < sliceLen; i++ { - v := data.Index(i).Interface() - sliceval := indirect(rv.Index(i)) - if err := md.unify(v, sliceval); err != nil { + l := data.Len() + for i := 0; i < l; i++ { + err := md.unify(data.Index(i).Interface(), indirect(rv.Index(i))) + if err != nil { return err } } return nil } -func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error { - if _, ok := data.(time.Time); ok { - rv.Set(reflect.ValueOf(data)) +func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error { + _, ok := rv.Interface().(json.Number) + if ok { + if i, ok := data.(int64); ok { + rv.SetString(strconv.FormatInt(i, 10)) + } else if f, ok := data.(float64); ok { + rv.SetString(strconv.FormatFloat(f, 'f', -1, 64)) + } else { + return md.badtype("string", data) + } return nil } - return badtype("time.Time", data) -} -func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error { if s, ok := data.(string); ok { rv.SetString(s) return nil } - return badtype("string", data) + return md.badtype("string", data) } func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error { + rvk := rv.Kind() + if num, ok := data.(float64); ok { - switch rv.Kind() { + switch rvk { case reflect.Float32: + if num < -math.MaxFloat32 || num > math.MaxFloat32 { + return md.parseErr(errParseRange{i: num, size: rvk.String()}) + } fallthrough case reflect.Float64: rv.SetFloat(num) @@ -376,54 +425,60 @@ func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error { } return nil } - return badtype("float", data) + + if num, ok := data.(int64); ok { + if (rvk == reflect.Float32 && (num < -maxSafeFloat32Int || num > maxSafeFloat32Int)) || + (rvk == reflect.Float64 && (num < -maxSafeFloat64Int || num > maxSafeFloat64Int)) { + return md.parseErr(errParseRange{i: num, size: rvk.String()}) + } + rv.SetFloat(float64(num)) + return nil + } + + return md.badtype("float", data) } func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error { - if num, ok := data.(int64); ok { - if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 { - switch rv.Kind() { - case reflect.Int, reflect.Int64: - // No bounds checking necessary. - case reflect.Int8: - if num < math.MinInt8 || num > math.MaxInt8 { - return e("value %d is out of range for int8", num) - } - case reflect.Int16: - if num < math.MinInt16 || num > math.MaxInt16 { - return e("value %d is out of range for int16", num) - } - case reflect.Int32: - if num < math.MinInt32 || num > math.MaxInt32 { - return e("value %d is out of range for int32", num) - } + _, ok := rv.Interface().(time.Duration) + if ok { + // Parse as string duration, and fall back to regular integer parsing + // (as nanosecond) if this is not a string. + if s, ok := data.(string); ok { + dur, err := time.ParseDuration(s) + if err != nil { + return md.parseErr(errParseDuration{s}) } - rv.SetInt(num) - } else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 { - unum := uint64(num) - switch rv.Kind() { - case reflect.Uint, reflect.Uint64: - // No bounds checking necessary. - case reflect.Uint8: - if num < 0 || unum > math.MaxUint8 { - return e("value %d is out of range for uint8", num) - } - case reflect.Uint16: - if num < 0 || unum > math.MaxUint16 { - return e("value %d is out of range for uint16", num) - } - case reflect.Uint32: - if num < 0 || unum > math.MaxUint32 { - return e("value %d is out of range for uint32", num) - } - } - rv.SetUint(unum) - } else { - panic("unreachable") + rv.SetInt(int64(dur)) + return nil } - return nil } - return badtype("integer", data) + + num, ok := data.(int64) + if !ok { + return md.badtype("integer", data) + } + + rvk := rv.Kind() + switch { + case rvk >= reflect.Int && rvk <= reflect.Int64: + if (rvk == reflect.Int8 && (num < math.MinInt8 || num > math.MaxInt8)) || + (rvk == reflect.Int16 && (num < math.MinInt16 || num > math.MaxInt16)) || + (rvk == reflect.Int32 && (num < math.MinInt32 || num > math.MaxInt32)) { + return md.parseErr(errParseRange{i: num, size: rvk.String()}) + } + rv.SetInt(num) + case rvk >= reflect.Uint && rvk <= reflect.Uint64: + unum := uint64(num) + if rvk == reflect.Uint8 && (num < 0 || unum > math.MaxUint8) || + rvk == reflect.Uint16 && (num < 0 || unum > math.MaxUint16) || + rvk == reflect.Uint32 && (num < 0 || unum > math.MaxUint32) { + return md.parseErr(errParseRange{i: num, size: rvk.String()}) + } + rv.SetUint(unum) + default: + panic("unreachable") + } + return nil } func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error { @@ -431,7 +486,7 @@ func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error { rv.SetBool(b) return nil } - return badtype("boolean", data) + return md.badtype("boolean", data) } func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error { @@ -439,10 +494,16 @@ func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error { return nil } -func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error { +func (md *MetaData) unifyText(data interface{}, v encoding.TextUnmarshaler) error { var s string switch sdata := data.(type) { - case TextMarshaler: + case Marshaler: + text, err := sdata.MarshalTOML() + if err != nil { + return err + } + s = string(text) + case encoding.TextMarshaler: text, err := sdata.MarshalText() if err != nil { return err @@ -459,7 +520,7 @@ func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error { case float64: s = fmt.Sprintf("%f", sdata) default: - return badtype("primitive (string-like)", data) + return md.badtype("primitive (string-like)", data) } if err := v.UnmarshalText([]byte(s)); err != nil { return err @@ -467,22 +528,54 @@ func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error { return nil } +func (md *MetaData) badtype(dst string, data interface{}) error { + return md.e("incompatible types: TOML value has type %T; destination has type %s", data, dst) +} + +func (md *MetaData) parseErr(err error) error { + k := md.context.String() + return ParseError{ + LastKey: k, + Position: md.keyInfo[k].pos, + Line: md.keyInfo[k].pos.Line, + err: err, + input: string(md.data), + } +} + +func (md *MetaData) e(format string, args ...interface{}) error { + f := "toml: " + if len(md.context) > 0 { + f = fmt.Sprintf("toml: (last key %q): ", md.context) + p := md.keyInfo[md.context.String()].pos + if p.Line > 0 { + f = fmt.Sprintf("toml: line %d (last key %q): ", p.Line, md.context) + } + } + return fmt.Errorf(f+format, args...) +} + // rvalue returns a reflect.Value of `v`. All pointers are resolved. func rvalue(v interface{}) reflect.Value { return indirect(reflect.ValueOf(v)) } // indirect returns the value pointed to by a pointer. -// Pointers are followed until the value is not a pointer. -// New values are allocated for each nil pointer. // -// An exception to this rule is if the value satisfies an interface of -// interest to us (like encoding.TextUnmarshaler). +// Pointers are followed until the value is not a pointer. New values are +// allocated for each nil pointer. +// +// An exception to this rule is if the value satisfies an interface of interest +// to us (like encoding.TextUnmarshaler). func indirect(v reflect.Value) reflect.Value { if v.Kind() != reflect.Ptr { if v.CanSet() { pv := v.Addr() - if _, ok := pv.Interface().(TextUnmarshaler); ok { + pvi := pv.Interface() + if _, ok := pvi.(encoding.TextUnmarshaler); ok { + return pv + } + if _, ok := pvi.(Unmarshaler); ok { return pv } } @@ -498,12 +591,12 @@ func isUnifiable(rv reflect.Value) bool { if rv.CanSet() { return true } - if _, ok := rv.Interface().(TextUnmarshaler); ok { + rvi := rv.Interface() + if _, ok := rvi.(encoding.TextUnmarshaler); ok { + return true + } + if _, ok := rvi.(Unmarshaler); ok { return true } return false } - -func badtype(expected string, data interface{}) error { - return e("cannot load TOML value of type %T into a Go %s", data, expected) -} diff --git a/vendor/github.com/BurntSushi/toml/decode_go116.go b/vendor/github.com/BurntSushi/toml/decode_go116.go new file mode 100644 index 00000000..086d0b68 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/decode_go116.go @@ -0,0 +1,19 @@ +//go:build go1.16 +// +build go1.16 + +package toml + +import ( + "io/fs" +) + +// DecodeFS reads the contents of a file from [fs.FS] and decodes it with +// [Decode]. +func DecodeFS(fsys fs.FS, path string, v interface{}) (MetaData, error) { + fp, err := fsys.Open(path) + if err != nil { + return MetaData{}, err + } + defer fp.Close() + return NewDecoder(fp).Decode(v) +} diff --git a/vendor/github.com/BurntSushi/toml/deprecated.go b/vendor/github.com/BurntSushi/toml/deprecated.go new file mode 100644 index 00000000..b9e30971 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/deprecated.go @@ -0,0 +1,29 @@ +package toml + +import ( + "encoding" + "io" +) + +// TextMarshaler is an alias for encoding.TextMarshaler. +// +// Deprecated: use encoding.TextMarshaler +type TextMarshaler encoding.TextMarshaler + +// TextUnmarshaler is an alias for encoding.TextUnmarshaler. +// +// Deprecated: use encoding.TextUnmarshaler +type TextUnmarshaler encoding.TextUnmarshaler + +// PrimitiveDecode is an alias for MetaData.PrimitiveDecode(). +// +// Deprecated: use MetaData.PrimitiveDecode. +func PrimitiveDecode(primValue Primitive, v interface{}) error { + md := MetaData{decoded: make(map[string]struct{})} + return md.unify(primValue.undecoded, rvalue(v)) +} + +// DecodeReader is an alias for NewDecoder(r).Decode(v). +// +// Deprecated: use NewDecoder(reader).Decode(&value). +func DecodeReader(r io.Reader, v interface{}) (MetaData, error) { return NewDecoder(r).Decode(v) } diff --git a/vendor/github.com/BurntSushi/toml/doc.go b/vendor/github.com/BurntSushi/toml/doc.go index b371f396..81a7c0fe 100644 --- a/vendor/github.com/BurntSushi/toml/doc.go +++ b/vendor/github.com/BurntSushi/toml/doc.go @@ -1,27 +1,11 @@ -/* -Package toml provides facilities for decoding and encoding TOML configuration -files via reflection. There is also support for delaying decoding with -the Primitive type, and querying the set of keys in a TOML document with the -MetaData type. - -The specification implemented: https://github.com/toml-lang/toml - -The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify -whether a file is a valid TOML document. It can also be used to print the -type of each key in a TOML document. - -Testing - -There are two important types of tests used for this package. The first is -contained inside '*_test.go' files and uses the standard Go unit testing -framework. These tests are primarily devoted to holistically testing the -decoder and encoder. - -The second type of testing is used to verify the implementation's adherence -to the TOML specification. These tests have been factored into their own -project: https://github.com/BurntSushi/toml-test - -The reason the tests are in a separate project is so that they can be used by -any implementation of TOML. Namely, it is language agnostic. -*/ +// Package toml implements decoding and encoding of TOML files. +// +// This package supports TOML v1.0.0, as specified at https://toml.io +// +// There is also support for delaying decoding with the Primitive type, and +// querying the set of keys in a TOML document with the MetaData type. +// +// The github.com/BurntSushi/toml/cmd/tomlv package implements a TOML validator, +// and can be used to verify if TOML document is valid. It can also be used to +// print the type of each key. package toml diff --git a/vendor/github.com/BurntSushi/toml/encode.go b/vendor/github.com/BurntSushi/toml/encode.go index d905c21a..9cd25d75 100644 --- a/vendor/github.com/BurntSushi/toml/encode.go +++ b/vendor/github.com/BurntSushi/toml/encode.go @@ -2,57 +2,127 @@ package toml import ( "bufio" + "encoding" + "encoding/json" "errors" "fmt" "io" + "math" "reflect" "sort" "strconv" "strings" "time" + + "github.com/BurntSushi/toml/internal" ) type tomlEncodeError struct{ error } var ( - errArrayMixedElementTypes = errors.New( - "toml: cannot encode array with mixed element types") - errArrayNilElement = errors.New( - "toml: cannot encode array with nil element") - errNonString = errors.New( - "toml: cannot encode a map with non-string key type") - errAnonNonStruct = errors.New( - "toml: cannot encode an anonymous field that is not a struct") - errArrayNoTable = errors.New( - "toml: TOML array element cannot contain a table") - errNoKey = errors.New( - "toml: top-level values must be Go maps or structs") - errAnything = errors.New("") // used in testing + errArrayNilElement = errors.New("toml: cannot encode array with nil element") + errNonString = errors.New("toml: cannot encode a map with non-string key type") + errNoKey = errors.New("toml: top-level values must be Go maps or structs") + errAnything = errors.New("") // used in testing ) -var quotedReplacer = strings.NewReplacer( - "\t", "\\t", - "\n", "\\n", - "\r", "\\r", +var dblQuotedReplacer = strings.NewReplacer( "\"", "\\\"", "\\", "\\\\", + "\x00", `\u0000`, + "\x01", `\u0001`, + "\x02", `\u0002`, + "\x03", `\u0003`, + "\x04", `\u0004`, + "\x05", `\u0005`, + "\x06", `\u0006`, + "\x07", `\u0007`, + "\b", `\b`, + "\t", `\t`, + "\n", `\n`, + "\x0b", `\u000b`, + "\f", `\f`, + "\r", `\r`, + "\x0e", `\u000e`, + "\x0f", `\u000f`, + "\x10", `\u0010`, + "\x11", `\u0011`, + "\x12", `\u0012`, + "\x13", `\u0013`, + "\x14", `\u0014`, + "\x15", `\u0015`, + "\x16", `\u0016`, + "\x17", `\u0017`, + "\x18", `\u0018`, + "\x19", `\u0019`, + "\x1a", `\u001a`, + "\x1b", `\u001b`, + "\x1c", `\u001c`, + "\x1d", `\u001d`, + "\x1e", `\u001e`, + "\x1f", `\u001f`, + "\x7f", `\u007f`, ) -// Encoder controls the encoding of Go values to a TOML document to some -// io.Writer. -// -// The indentation level can be controlled with the Indent field. -type Encoder struct { - // A single indentation level. By default it is two spaces. - Indent string +var ( + marshalToml = reflect.TypeOf((*Marshaler)(nil)).Elem() + marshalText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() + timeType = reflect.TypeOf((*time.Time)(nil)).Elem() +) - // hasWritten is whether we have written any output to w yet. - hasWritten bool - w *bufio.Writer +// Marshaler is the interface implemented by types that can marshal themselves +// into valid TOML. +type Marshaler interface { + MarshalTOML() ([]byte, error) } -// NewEncoder returns a TOML encoder that encodes Go values to the io.Writer -// given. By default, a single indentation level is 2 spaces. +// Encoder encodes a Go to a TOML document. +// +// The mapping between Go values and TOML values should be precisely the same as +// for [Decode]. +// +// time.Time is encoded as a RFC 3339 string, and time.Duration as its string +// representation. +// +// The [Marshaler] and [encoding.TextMarshaler] interfaces are supported to +// encoding the value as custom TOML. +// +// If you want to write arbitrary binary data then you will need to use +// something like base64 since TOML does not have any binary types. +// +// When encoding TOML hashes (Go maps or structs), keys without any sub-hashes +// are encoded first. +// +// Go maps will be sorted alphabetically by key for deterministic output. +// +// The toml struct tag can be used to provide the key name; if omitted the +// struct field name will be used. If the "omitempty" option is present the +// following value will be skipped: +// +// - arrays, slices, maps, and string with len of 0 +// - struct with all zero values +// - bool false +// +// If omitzero is given all int and float types with a value of 0 will be +// skipped. +// +// Encoding Go values without a corresponding TOML representation will return an +// error. Examples of this includes maps with non-string keys, slices with nil +// elements, embedded non-struct types, and nested slices containing maps or +// structs. (e.g. [][]map[string]string is not allowed but []map[string]string +// is okay, as is []map[string][]string). +// +// NOTE: only exported keys are encoded due to the use of reflection. Unexported +// keys are silently discarded. +type Encoder struct { + // String to use for a single indentation level; default is two spaces. + Indent string + + w *bufio.Writer + hasWritten bool // written any output to w yet? +} + +// NewEncoder create a new Encoder. func NewEncoder(w io.Writer) *Encoder { return &Encoder{ w: bufio.NewWriter(w), @@ -60,32 +130,14 @@ func NewEncoder(w io.Writer) *Encoder { } } -// Encode writes a TOML representation of the Go value to the underlying -// io.Writer. If the value given cannot be encoded to a valid TOML document, -// then an error is returned. +// Encode writes a TOML representation of the Go value to the [Encoder]'s writer. // -// The mapping between Go values and TOML values should be precisely the same -// as for the Decode* functions. Similarly, the TextMarshaler interface is -// supported by encoding the resulting bytes as strings. (If you want to write -// arbitrary binary data then you will need to use something like base64 since -// TOML does not have any binary types.) -// -// When encoding TOML hashes (i.e., Go maps or structs), keys without any -// sub-hashes are encoded first. -// -// If a Go map is encoded, then its keys are sorted alphabetically for -// deterministic output. More control over this behavior may be provided if -// there is demand for it. -// -// Encoding Go values without a corresponding TOML representation---like map -// types with non-string keys---will cause an error to be returned. Similarly -// for mixed arrays/slices, arrays/slices with nil elements, embedded -// non-struct types and nested slices containing maps or structs. -// (e.g., [][]map[string]string is not allowed but []map[string]string is OK -// and so is []map[string][]string.) +// An error is returned if the value given cannot be encoded to a valid TOML +// document. func (enc *Encoder) Encode(v interface{}) error { rv := eindirect(reflect.ValueOf(v)) - if err := enc.safeEncode(Key([]string{}), rv); err != nil { + err := enc.safeEncode(Key([]string{}), rv) + if err != nil { return err } return enc.w.Flush() @@ -106,13 +158,15 @@ func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) { } func (enc *Encoder) encode(key Key, rv reflect.Value) { - // Special case. Time needs to be in ISO8601 format. - // Special case. If we can marshal the type to text, then we used that. - // Basically, this prevents the encoder for handling these types as - // generic structs (or whatever the underlying type of a TextMarshaler is). - switch rv.Interface().(type) { - case time.Time, TextMarshaler: - enc.keyEqElement(key, rv) + // If we can marshal the type to text, then we use that. This prevents the + // encoder for handling these types as generic structs (or whatever the + // underlying type of a TextMarshaler is). + switch { + case isMarshaler(rv): + enc.writeKeyValue(key, rv, false) + return + case rv.Type() == primitiveType: // TODO: #76 would make this superfluous after implemented. + enc.encode(key, reflect.ValueOf(rv.Interface().(Primitive).undecoded)) return } @@ -123,12 +177,12 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) { reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.String, reflect.Bool: - enc.keyEqElement(key, rv) + enc.writeKeyValue(key, rv, false) case reflect.Array, reflect.Slice: if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) { enc.eArrayOfTables(key, rv) } else { - enc.keyEqElement(key, rv) + enc.writeKeyValue(key, rv, false) } case reflect.Interface: if rv.IsNil() { @@ -148,55 +202,114 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) { case reflect.Struct: enc.eTable(key, rv) default: - panic(e("unsupported type for key '%s': %s", key, k)) + encPanic(fmt.Errorf("unsupported type for key '%s': %s", key, k)) } } -// eElement encodes any value that can be an array element (primitives and -// arrays). +// eElement encodes any value that can be an array element. func (enc *Encoder) eElement(rv reflect.Value) { switch v := rv.Interface().(type) { - case time.Time: - // Special case time.Time as a primitive. Has to come before - // TextMarshaler below because time.Time implements - // encoding.TextMarshaler, but we need to always use UTC. - enc.wf(v.UTC().Format("2006-01-02T15:04:05Z")) - return - case TextMarshaler: - // Special case. Use text marshaler if it's available for this value. - if s, err := v.MarshalText(); err != nil { - encPanic(err) - } else { - enc.writeQuoted(string(s)) + case time.Time: // Using TextMarshaler adds extra quotes, which we don't want. + format := time.RFC3339Nano + switch v.Location() { + case internal.LocalDatetime: + format = "2006-01-02T15:04:05.999999999" + case internal.LocalDate: + format = "2006-01-02" + case internal.LocalTime: + format = "15:04:05.999999999" + } + switch v.Location() { + default: + enc.wf(v.Format(format)) + case internal.LocalDatetime, internal.LocalDate, internal.LocalTime: + enc.wf(v.In(time.UTC).Format(format)) } return + case Marshaler: + s, err := v.MarshalTOML() + if err != nil { + encPanic(err) + } + if s == nil { + encPanic(errors.New("MarshalTOML returned nil and no error")) + } + enc.w.Write(s) + return + case encoding.TextMarshaler: + s, err := v.MarshalText() + if err != nil { + encPanic(err) + } + if s == nil { + encPanic(errors.New("MarshalText returned nil and no error")) + } + enc.writeQuoted(string(s)) + return + case time.Duration: + enc.writeQuoted(v.String()) + return + case json.Number: + n, _ := rv.Interface().(json.Number) + + if n == "" { /// Useful zero value. + enc.w.WriteByte('0') + return + } else if v, err := n.Int64(); err == nil { + enc.eElement(reflect.ValueOf(v)) + return + } else if v, err := n.Float64(); err == nil { + enc.eElement(reflect.ValueOf(v)) + return + } + encPanic(fmt.Errorf("unable to convert %q to int64 or float64", n)) } + switch rv.Kind() { - case reflect.Bool: - enc.wf(strconv.FormatBool(rv.Bool())) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, - reflect.Int64: - enc.wf(strconv.FormatInt(rv.Int(), 10)) - case reflect.Uint, reflect.Uint8, reflect.Uint16, - reflect.Uint32, reflect.Uint64: - enc.wf(strconv.FormatUint(rv.Uint(), 10)) - case reflect.Float32: - enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32))) - case reflect.Float64: - enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64))) - case reflect.Array, reflect.Slice: - enc.eArrayOrSliceElement(rv) - case reflect.Interface: + case reflect.Ptr: enc.eElement(rv.Elem()) + return case reflect.String: enc.writeQuoted(rv.String()) + case reflect.Bool: + enc.wf(strconv.FormatBool(rv.Bool())) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + enc.wf(strconv.FormatInt(rv.Int(), 10)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + enc.wf(strconv.FormatUint(rv.Uint(), 10)) + case reflect.Float32: + f := rv.Float() + if math.IsNaN(f) { + enc.wf("nan") + } else if math.IsInf(f, 0) { + enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)]) + } else { + enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32))) + } + case reflect.Float64: + f := rv.Float() + if math.IsNaN(f) { + enc.wf("nan") + } else if math.IsInf(f, 0) { + enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)]) + } else { + enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64))) + } + case reflect.Array, reflect.Slice: + enc.eArrayOrSliceElement(rv) + case reflect.Struct: + enc.eStruct(nil, rv, true) + case reflect.Map: + enc.eMap(nil, rv, true) + case reflect.Interface: + enc.eElement(rv.Elem()) default: - panic(e("unexpected primitive type: %s", rv.Kind())) + encPanic(fmt.Errorf("unexpected type: %T", rv.Interface())) } } -// By the TOML spec, all floats must have a decimal with at least one -// number on either side. +// By the TOML spec, all floats must have a decimal with at least one number on +// either side. func floatAddDecimal(fstr string) string { if !strings.Contains(fstr, ".") { return fstr + ".0" @@ -205,14 +318,14 @@ func floatAddDecimal(fstr string) string { } func (enc *Encoder) writeQuoted(s string) { - enc.wf("\"%s\"", quotedReplacer.Replace(s)) + enc.wf("\"%s\"", dblQuotedReplacer.Replace(s)) } func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) { length := rv.Len() enc.wf("[") for i := 0; i < length; i++ { - elem := rv.Index(i) + elem := eindirect(rv.Index(i)) enc.eElement(elem) if i != length-1 { enc.wf(", ") @@ -226,44 +339,43 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) { encPanic(errNoKey) } for i := 0; i < rv.Len(); i++ { - trv := rv.Index(i) + trv := eindirect(rv.Index(i)) if isNil(trv) { continue } - panicIfInvalidKey(key) enc.newline() - enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll()) + enc.wf("%s[[%s]]", enc.indentStr(key), key) enc.newline() - enc.eMapOrStruct(key, trv) + enc.eMapOrStruct(key, trv, false) } } func (enc *Encoder) eTable(key Key, rv reflect.Value) { - panicIfInvalidKey(key) if len(key) == 1 { // Output an extra newline between top-level tables. // (The newline isn't written if nothing else has been written though.) enc.newline() } if len(key) > 0 { - enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll()) + enc.wf("%s[%s]", enc.indentStr(key), key) enc.newline() } - enc.eMapOrStruct(key, rv) + enc.eMapOrStruct(key, rv, false) } -func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) { - switch rv := eindirect(rv); rv.Kind() { +func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) { + switch rv.Kind() { case reflect.Map: - enc.eMap(key, rv) + enc.eMap(key, rv, inline) case reflect.Struct: - enc.eStruct(key, rv) + enc.eStruct(key, rv, inline) default: + // Should never happen? panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String()) } } -func (enc *Encoder) eMap(key Key, rv reflect.Value) { +func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) { rt := rv.Type() if rt.Key().Kind() != reflect.String { encPanic(errNonString) @@ -274,68 +386,100 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value) { var mapKeysDirect, mapKeysSub []string for _, mapKey := range rv.MapKeys() { k := mapKey.String() - if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) { + if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) { mapKeysSub = append(mapKeysSub, k) } else { mapKeysDirect = append(mapKeysDirect, k) } } - var writeMapKeys = func(mapKeys []string) { + var writeMapKeys = func(mapKeys []string, trailC bool) { sort.Strings(mapKeys) - for _, mapKey := range mapKeys { - mrv := rv.MapIndex(reflect.ValueOf(mapKey)) - if isNil(mrv) { - // Don't write anything for nil fields. + for i, mapKey := range mapKeys { + val := eindirect(rv.MapIndex(reflect.ValueOf(mapKey))) + if isNil(val) { continue } - enc.encode(key.add(mapKey), mrv) + + if inline { + enc.writeKeyValue(Key{mapKey}, val, true) + if trailC || i != len(mapKeys)-1 { + enc.wf(", ") + } + } else { + enc.encode(key.add(mapKey), val) + } } } - writeMapKeys(mapKeysDirect) - writeMapKeys(mapKeysSub) + + if inline { + enc.wf("{") + } + writeMapKeys(mapKeysDirect, len(mapKeysSub) > 0) + writeMapKeys(mapKeysSub, false) + if inline { + enc.wf("}") + } } -func (enc *Encoder) eStruct(key Key, rv reflect.Value) { +const is32Bit = (32 << (^uint(0) >> 63)) == 32 + +func pointerTo(t reflect.Type) reflect.Type { + if t.Kind() == reflect.Ptr { + return pointerTo(t.Elem()) + } + return t +} + +func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) { // Write keys for fields directly under this key first, because if we write - // a field that creates a new table, then all keys under it will be in that + // a field that creates a new table then all keys under it will be in that // table (not the one we're writing here). - rt := rv.Type() - var fieldsDirect, fieldsSub [][]int - var addFields func(rt reflect.Type, rv reflect.Value, start []int) + // + // Fields is a [][]int: for fieldsDirect this always has one entry (the + // struct index). For fieldsSub it contains two entries: the parent field + // index from tv, and the field indexes for the fields of the sub. + var ( + rt = rv.Type() + fieldsDirect, fieldsSub [][]int + addFields func(rt reflect.Type, rv reflect.Value, start []int) + ) addFields = func(rt reflect.Type, rv reflect.Value, start []int) { for i := 0; i < rt.NumField(); i++ { f := rt.Field(i) - // skip unexported fields - if f.PkgPath != "" && !f.Anonymous { + isEmbed := f.Anonymous && pointerTo(f.Type).Kind() == reflect.Struct + if f.PkgPath != "" && !isEmbed { /// Skip unexported fields. continue } - frv := rv.Field(i) - if f.Anonymous { - t := f.Type - switch t.Kind() { - case reflect.Struct: - // Treat anonymous struct fields with - // tag names as though they are not - // anonymous, like encoding/json does. - if getOptions(f.Tag).name == "" { - addFields(t, frv, f.Index) - continue - } - case reflect.Ptr: - if t.Elem().Kind() == reflect.Struct && - getOptions(f.Tag).name == "" { - if !frv.IsNil() { - addFields(t.Elem(), frv.Elem(), f.Index) - } - continue - } - // Fall through to the normal field encoding logic below - // for non-struct anonymous fields. + opts := getOptions(f.Tag) + if opts.skip { + continue + } + + frv := eindirect(rv.Field(i)) + + if is32Bit { + // Copy so it works correct on 32bit archs; not clear why this + // is needed. See #314, and https://www.reddit.com/r/golang/comments/pnx8v4 + // This also works fine on 64bit, but 32bit archs are somewhat + // rare and this is a wee bit faster. + copyStart := make([]int, len(start)) + copy(copyStart, start) + start = copyStart + } + + // Treat anonymous struct fields with tag names as though they are + // not anonymous, like encoding/json does. + // + // Non-struct anonymous fields use the normal encoding logic. + if isEmbed { + if getOptions(f.Tag).name == "" && frv.Kind() == reflect.Struct { + addFields(frv.Type(), frv, append(start, f.Index...)) + continue } } - if typeIsHash(tomlTypeOfGo(frv)) { + if typeIsTable(tomlTypeOfGo(frv)) { fieldsSub = append(fieldsSub, append(start, f.Index...)) } else { fieldsDirect = append(fieldsDirect, append(start, f.Index...)) @@ -344,48 +488,81 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value) { } addFields(rt, rv, nil) - var writeFields = func(fields [][]int) { + writeFields := func(fields [][]int) { for _, fieldIndex := range fields { - sft := rt.FieldByIndex(fieldIndex) - sf := rv.FieldByIndex(fieldIndex) - if isNil(sf) { - // Don't write anything for nil fields. - continue - } + fieldType := rt.FieldByIndex(fieldIndex) + fieldVal := rv.FieldByIndex(fieldIndex) - opts := getOptions(sft.Tag) + opts := getOptions(fieldType.Tag) if opts.skip { continue } - keyName := sft.Name + if opts.omitempty && isEmpty(fieldVal) { + continue + } + + fieldVal = eindirect(fieldVal) + + if isNil(fieldVal) { /// Don't write anything for nil fields. + continue + } + + keyName := fieldType.Name if opts.name != "" { keyName = opts.name } - if opts.omitempty && isEmpty(sf) { - continue - } - if opts.omitzero && isZero(sf) { + + if opts.omitzero && isZero(fieldVal) { continue } - enc.encode(key.add(keyName), sf) + if inline { + enc.writeKeyValue(Key{keyName}, fieldVal, true) + if fieldIndex[0] != len(fields)-1 { + enc.wf(", ") + } + } else { + enc.encode(key.add(keyName), fieldVal) + } } } + + if inline { + enc.wf("{") + } writeFields(fieldsDirect) writeFields(fieldsSub) + if inline { + enc.wf("}") + } } -// tomlTypeName returns the TOML type name of the Go value's type. It is -// used to determine whether the types of array elements are mixed (which is -// forbidden). If the Go value is nil, then it is illegal for it to be an array -// element, and valueIsNil is returned as true. - -// Returns the TOML type of a Go value. The type may be `nil`, which means -// no concrete TOML type could be found. +// tomlTypeOfGo returns the TOML type name of the Go value's type. +// +// It is used to determine whether the types of array elements are mixed (which +// is forbidden). If the Go value is nil, then it is illegal for it to be an +// array element, and valueIsNil is returned as true. +// +// The type may be `nil`, which means no concrete TOML type could be found. func tomlTypeOfGo(rv reflect.Value) tomlType { if isNil(rv) || !rv.IsValid() { return nil } + + if rv.Kind() == reflect.Struct { + if rv.Type() == timeType { + return tomlDatetime + } + if isMarshaler(rv) { + return tomlString + } + return tomlHash + } + + if isMarshaler(rv) { + return tomlString + } + switch rv.Kind() { case reflect.Bool: return tomlBool @@ -397,7 +574,7 @@ func tomlTypeOfGo(rv reflect.Value) tomlType { case reflect.Float32, reflect.Float64: return tomlFloat case reflect.Array, reflect.Slice: - if typeEqual(tomlHash, tomlArrayType(rv)) { + if isTableArray(rv) { return tomlArrayHash } return tomlArray @@ -407,54 +584,35 @@ func tomlTypeOfGo(rv reflect.Value) tomlType { return tomlString case reflect.Map: return tomlHash - case reflect.Struct: - switch rv.Interface().(type) { - case time.Time: - return tomlDatetime - case TextMarshaler: - return tomlString - default: - return tomlHash - } default: - panic("unexpected reflect.Kind: " + rv.Kind().String()) + encPanic(errors.New("unsupported type: " + rv.Kind().String())) + panic("unreachable") } } -// tomlArrayType returns the element type of a TOML array. The type returned -// may be nil if it cannot be determined (e.g., a nil slice or a zero length -// slize). This function may also panic if it finds a type that cannot be -// expressed in TOML (such as nil elements, heterogeneous arrays or directly -// nested arrays of tables). -func tomlArrayType(rv reflect.Value) tomlType { - if isNil(rv) || !rv.IsValid() || rv.Len() == 0 { - return nil - } - firstType := tomlTypeOfGo(rv.Index(0)) - if firstType == nil { - encPanic(errArrayNilElement) +func isMarshaler(rv reflect.Value) bool { + return rv.Type().Implements(marshalText) || rv.Type().Implements(marshalToml) +} + +// isTableArray reports if all entries in the array or slice are a table. +func isTableArray(arr reflect.Value) bool { + if isNil(arr) || !arr.IsValid() || arr.Len() == 0 { + return false } - rvlen := rv.Len() - for i := 1; i < rvlen; i++ { - elem := rv.Index(i) - switch elemType := tomlTypeOfGo(elem); { - case elemType == nil: + ret := true + for i := 0; i < arr.Len(); i++ { + tt := tomlTypeOfGo(eindirect(arr.Index(i))) + // Don't allow nil. + if tt == nil { encPanic(errArrayNilElement) - case !typeEqual(firstType, elemType): - encPanic(errArrayMixedElementTypes) + } + + if ret && !typeEqual(tomlHash, tt) { + ret = false } } - // If we have a nested array, then we must make sure that the nested - // array contains ONLY primitives. - // This checks arbitrarily nested arrays. - if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) { - nest := tomlArrayType(eindirect(rv.Index(0))) - if typeEqual(nest, tomlHash) || typeEqual(nest, tomlArrayHash) { - encPanic(errArrayNoTable) - } - } - return firstType + return ret } type tagOptions struct { @@ -499,8 +657,26 @@ func isEmpty(rv reflect.Value) bool { switch rv.Kind() { case reflect.Array, reflect.Slice, reflect.Map, reflect.String: return rv.Len() == 0 + case reflect.Struct: + if rv.Type().Comparable() { + return reflect.Zero(rv.Type()).Interface() == rv.Interface() + } + // Need to also check if all the fields are empty, otherwise something + // like this with uncomparable types will always return true: + // + // type a struct{ field b } + // type b struct{ s []string } + // s := a{field: b{s: []string{"AAA"}}} + for i := 0; i < rv.NumField(); i++ { + if !isEmpty(rv.Field(i)) { + return false + } + } + return true case reflect.Bool: return !rv.Bool() + case reflect.Ptr: + return rv.IsNil() } return false } @@ -511,18 +687,34 @@ func (enc *Encoder) newline() { } } -func (enc *Encoder) keyEqElement(key Key, val reflect.Value) { +// Write a key/value pair: +// +// key = +// +// This is also used for "k = v" in inline tables; so something like this will +// be written in three calls: +// +// ┌───────────────────┐ +// │ ┌───┐ ┌────┐│ +// v v v v vv +// key = {k = 1, k2 = 2} +func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) { + /// Marshaler used on top-level document; call eElement() to just call + /// Marshal{TOML,Text}. if len(key) == 0 { - encPanic(errNoKey) + enc.eElement(val) + return } - panicIfInvalidKey(key) enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1)) enc.eElement(val) - enc.newline() + if !inline { + enc.newline() + } } func (enc *Encoder) wf(format string, v ...interface{}) { - if _, err := fmt.Fprintf(enc.w, format, v...); err != nil { + _, err := fmt.Fprintf(enc.w, format, v...) + if err != nil { encPanic(err) } enc.hasWritten = true @@ -536,13 +728,25 @@ func encPanic(err error) { panic(tomlEncodeError{err}) } +// Resolve any level of pointers to the actual value (e.g. **string → string). func eindirect(v reflect.Value) reflect.Value { - switch v.Kind() { - case reflect.Ptr, reflect.Interface: - return eindirect(v.Elem()) - default: + if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface { + if isMarshaler(v) { + return v + } + if v.CanAddr() { /// Special case for marshalers; see #358. + if pv := v.Addr(); isMarshaler(pv) { + return pv + } + } return v } + + if v.IsNil() { + return v + } + + return eindirect(v.Elem()) } func isNil(rv reflect.Value) bool { @@ -553,16 +757,3 @@ func isNil(rv reflect.Value) bool { return false } } - -func panicIfInvalidKey(key Key) { - for _, k := range key { - if len(k) == 0 { - encPanic(e("Key '%s' is not a valid table name. Key names "+ - "cannot be empty.", key.maybeQuotedAll())) - } - } -} - -func isValidKeyName(s string) bool { - return len(s) != 0 -} diff --git a/vendor/github.com/BurntSushi/toml/encoding_types.go b/vendor/github.com/BurntSushi/toml/encoding_types.go deleted file mode 100644 index d36e1dd6..00000000 --- a/vendor/github.com/BurntSushi/toml/encoding_types.go +++ /dev/null @@ -1,19 +0,0 @@ -// +build go1.2 - -package toml - -// In order to support Go 1.1, we define our own TextMarshaler and -// TextUnmarshaler types. For Go 1.2+, we just alias them with the -// standard library interfaces. - -import ( - "encoding" -) - -// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here -// so that Go 1.1 can be supported. -type TextMarshaler encoding.TextMarshaler - -// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined -// here so that Go 1.1 can be supported. -type TextUnmarshaler encoding.TextUnmarshaler diff --git a/vendor/github.com/BurntSushi/toml/encoding_types_1.1.go b/vendor/github.com/BurntSushi/toml/encoding_types_1.1.go deleted file mode 100644 index e8d503d0..00000000 --- a/vendor/github.com/BurntSushi/toml/encoding_types_1.1.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build !go1.2 - -package toml - -// These interfaces were introduced in Go 1.2, so we add them manually when -// compiling for Go 1.1. - -// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here -// so that Go 1.1 can be supported. -type TextMarshaler interface { - MarshalText() (text []byte, err error) -} - -// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined -// here so that Go 1.1 can be supported. -type TextUnmarshaler interface { - UnmarshalText(text []byte) error -} diff --git a/vendor/github.com/BurntSushi/toml/error.go b/vendor/github.com/BurntSushi/toml/error.go new file mode 100644 index 00000000..efd68865 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/error.go @@ -0,0 +1,279 @@ +package toml + +import ( + "fmt" + "strings" +) + +// ParseError is returned when there is an error parsing the TOML syntax such as +// invalid syntax, duplicate keys, etc. +// +// In addition to the error message itself, you can also print detailed location +// information with context by using [ErrorWithPosition]: +// +// toml: error: Key 'fruit' was already created and cannot be used as an array. +// +// At line 4, column 2-7: +// +// 2 | fruit = [] +// 3 | +// 4 | [[fruit]] # Not allowed +// ^^^^^ +// +// [ErrorWithUsage] can be used to print the above with some more detailed usage +// guidance: +// +// toml: error: newlines not allowed within inline tables +// +// At line 1, column 18: +// +// 1 | x = [{ key = 42 # +// ^ +// +// Error help: +// +// Inline tables must always be on a single line: +// +// table = {key = 42, second = 43} +// +// It is invalid to split them over multiple lines like so: +// +// # INVALID +// table = { +// key = 42, +// second = 43 +// } +// +// Use regular for this: +// +// [table] +// key = 42 +// second = 43 +type ParseError struct { + Message string // Short technical message. + Usage string // Longer message with usage guidance; may be blank. + Position Position // Position of the error + LastKey string // Last parsed key, may be blank. + + // Line the error occurred. + // + // Deprecated: use [Position]. + Line int + + err error + input string +} + +// Position of an error. +type Position struct { + Line int // Line number, starting at 1. + Start int // Start of error, as byte offset starting at 0. + Len int // Lenght in bytes. +} + +func (pe ParseError) Error() string { + msg := pe.Message + if msg == "" { // Error from errorf() + msg = pe.err.Error() + } + + if pe.LastKey == "" { + return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, msg) + } + return fmt.Sprintf("toml: line %d (last key %q): %s", + pe.Position.Line, pe.LastKey, msg) +} + +// ErrorWithPosition returns the error with detailed location context. +// +// See the documentation on [ParseError]. +func (pe ParseError) ErrorWithPosition() string { + if pe.input == "" { // Should never happen, but just in case. + return pe.Error() + } + + var ( + lines = strings.Split(pe.input, "\n") + col = pe.column(lines) + b = new(strings.Builder) + ) + + msg := pe.Message + if msg == "" { + msg = pe.err.Error() + } + + // TODO: don't show control characters as literals? This may not show up + // well everywhere. + + if pe.Position.Len == 1 { + fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n", + msg, pe.Position.Line, col+1) + } else { + fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n", + msg, pe.Position.Line, col, col+pe.Position.Len) + } + if pe.Position.Line > 2 { + fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, lines[pe.Position.Line-3]) + } + if pe.Position.Line > 1 { + fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, lines[pe.Position.Line-2]) + } + fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, lines[pe.Position.Line-1]) + fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col), strings.Repeat("^", pe.Position.Len)) + return b.String() +} + +// ErrorWithUsage returns the error with detailed location context and usage +// guidance. +// +// See the documentation on [ParseError]. +func (pe ParseError) ErrorWithUsage() string { + m := pe.ErrorWithPosition() + if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" { + lines := strings.Split(strings.TrimSpace(u.Usage()), "\n") + for i := range lines { + if lines[i] != "" { + lines[i] = " " + lines[i] + } + } + return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n" + } + return m +} + +func (pe ParseError) column(lines []string) int { + var pos, col int + for i := range lines { + ll := len(lines[i]) + 1 // +1 for the removed newline + if pos+ll >= pe.Position.Start { + col = pe.Position.Start - pos + if col < 0 { // Should never happen, but just in case. + col = 0 + } + break + } + pos += ll + } + + return col +} + +type ( + errLexControl struct{ r rune } + errLexEscape struct{ r rune } + errLexUTF8 struct{ b byte } + errLexInvalidNum struct{ v string } + errLexInvalidDate struct{ v string } + errLexInlineTableNL struct{} + errLexStringNL struct{} + errParseRange struct { + i interface{} // int or float + size string // "int64", "uint16", etc. + } + errParseDuration struct{ d string } +) + +func (e errLexControl) Error() string { + return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r) +} +func (e errLexControl) Usage() string { return "" } + +func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) } +func (e errLexEscape) Usage() string { return usageEscape } +func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) } +func (e errLexUTF8) Usage() string { return "" } +func (e errLexInvalidNum) Error() string { return fmt.Sprintf("invalid number: %q", e.v) } +func (e errLexInvalidNum) Usage() string { return "" } +func (e errLexInvalidDate) Error() string { return fmt.Sprintf("invalid date: %q", e.v) } +func (e errLexInvalidDate) Usage() string { return "" } +func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" } +func (e errLexInlineTableNL) Usage() string { return usageInlineNewline } +func (e errLexStringNL) Error() string { return "strings cannot contain newlines" } +func (e errLexStringNL) Usage() string { return usageStringNewline } +func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) } +func (e errParseRange) Usage() string { return usageIntOverflow } +func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) } +func (e errParseDuration) Usage() string { return usageDuration } + +const usageEscape = ` +A '\' inside a "-delimited string is interpreted as an escape character. + +The following escape sequences are supported: +\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX + +To prevent a '\' from being recognized as an escape character, use either: + +- a ' or '''-delimited string; escape characters aren't processed in them; or +- write two backslashes to get a single backslash: '\\'. + +If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/' +instead of '\' will usually also work: "C:/Users/martin". +` + +const usageInlineNewline = ` +Inline tables must always be on a single line: + + table = {key = 42, second = 43} + +It is invalid to split them over multiple lines like so: + + # INVALID + table = { + key = 42, + second = 43 + } + +Use regular for this: + + [table] + key = 42 + second = 43 +` + +const usageStringNewline = ` +Strings must always be on a single line, and cannot span more than one line: + + # INVALID + string = "Hello, + world!" + +Instead use """ or ''' to split strings over multiple lines: + + string = """Hello, + world!""" +` + +const usageIntOverflow = ` +This number is too large; this may be an error in the TOML, but it can also be a +bug in the program that uses too small of an integer. + +The maximum and minimum values are: + + size │ lowest │ highest + ───────┼────────────────┼────────── + int8 │ -128 │ 127 + int16 │ -32,768 │ 32,767 + int32 │ -2,147,483,648 │ 2,147,483,647 + int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷ + uint8 │ 0 │ 255 + uint16 │ 0 │ 65535 + uint32 │ 0 │ 4294967295 + uint64 │ 0 │ 1.8 × 10¹⁸ + +int refers to int32 on 32-bit systems and int64 on 64-bit systems. +` + +const usageDuration = ` +A duration must be as "number", without any spaces. Valid units are: + + ns nanoseconds (billionth of a second) + us, µs microseconds (millionth of a second) + ms milliseconds (thousands of a second) + s seconds + m minutes + h hours + +You can combine multiple units; for example "5m10s" for 5 minutes and 10 +seconds. +` diff --git a/vendor/github.com/BurntSushi/toml/internal/tz.go b/vendor/github.com/BurntSushi/toml/internal/tz.go new file mode 100644 index 00000000..022f15bc --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/internal/tz.go @@ -0,0 +1,36 @@ +package internal + +import "time" + +// Timezones used for local datetime, date, and time TOML types. +// +// The exact way times and dates without a timezone should be interpreted is not +// well-defined in the TOML specification and left to the implementation. These +// defaults to current local timezone offset of the computer, but this can be +// changed by changing these variables before decoding. +// +// TODO: +// Ideally we'd like to offer people the ability to configure the used timezone +// by setting Decoder.Timezone and Encoder.Timezone; however, this is a bit +// tricky: the reason we use three different variables for this is to support +// round-tripping – without these specific TZ names we wouldn't know which +// format to use. +// +// There isn't a good way to encode this right now though, and passing this sort +// of information also ties in to various related issues such as string format +// encoding, encoding of comments, etc. +// +// So, for the time being, just put this in internal until we can write a good +// comprehensive API for doing all of this. +// +// The reason they're exported is because they're referred from in e.g. +// internal/tag. +// +// Note that this behaviour is valid according to the TOML spec as the exact +// behaviour is left up to implementations. +var ( + localOffset = func() int { _, o := time.Now().Zone(); return o }() + LocalDatetime = time.FixedZone("datetime-local", localOffset) + LocalDate = time.FixedZone("date-local", localOffset) + LocalTime = time.FixedZone("time-local", localOffset) +) diff --git a/vendor/github.com/BurntSushi/toml/lex.go b/vendor/github.com/BurntSushi/toml/lex.go index e0a742a8..3545a6ad 100644 --- a/vendor/github.com/BurntSushi/toml/lex.go +++ b/vendor/github.com/BurntSushi/toml/lex.go @@ -2,6 +2,8 @@ package toml import ( "fmt" + "reflect" + "runtime" "strings" "unicode" "unicode/utf8" @@ -29,61 +31,49 @@ const ( itemArrayTableStart itemArrayTableEnd itemKeyStart + itemKeyEnd itemCommentStart itemInlineTableStart itemInlineTableEnd ) -const ( - eof = 0 - comma = ',' - tableStart = '[' - tableEnd = ']' - arrayTableStart = '[' - arrayTableEnd = ']' - tableSep = '.' - keySep = '=' - arrayStart = '[' - arrayEnd = ']' - commentStart = '#' - stringStart = '"' - stringEnd = '"' - rawStringStart = '\'' - rawStringEnd = '\'' - inlineTableStart = '{' - inlineTableEnd = '}' -) +const eof = 0 type stateFn func(lx *lexer) stateFn -type lexer struct { - input string - start int - pos int - line int - state stateFn - items chan item +func (p Position) String() string { + return fmt.Sprintf("at line %d; start %d; length %d", p.Line, p.Start, p.Len) +} - // Allow for backing up up to three runes. - // This is necessary because TOML contains 3-rune tokens (""" and '''). - prevWidths [3]int - nprev int // how many of prevWidths are in use - // If we emit an eof, we can still back up, but it is not OK to call - // next again. - atEOF bool +type lexer struct { + input string + start int + pos int + line int + state stateFn + items chan item + tomlNext bool + + // Allow for backing up up to 4 runes. This is necessary because TOML + // contains 3-rune tokens (""" and '''). + prevWidths [4]int + nprev int // how many of prevWidths are in use + atEOF bool // If we emit an eof, we can still back up, but it is not OK to call next again. // A stack of state functions used to maintain context. - // The idea is to reuse parts of the state machine in various places. - // For example, values can appear at the top level or within arbitrarily - // nested arrays. The last state on the stack is used after a value has - // been lexed. Similarly for comments. + // + // The idea is to reuse parts of the state machine in various places. For + // example, values can appear at the top level or within arbitrarily nested + // arrays. The last state on the stack is used after a value has been lexed. + // Similarly for comments. stack []stateFn } type item struct { - typ itemType - val string - line int + typ itemType + val string + err error + pos Position } func (lx *lexer) nextItem() item { @@ -93,17 +83,19 @@ func (lx *lexer) nextItem() item { return item default: lx.state = lx.state(lx) + //fmt.Printf(" STATE %-24s current: %-10s stack: %s\n", lx.state, lx.current(), lx.stack) } } } -func lex(input string) *lexer { +func lex(input string, tomlNext bool) *lexer { lx := &lexer{ - input: input, - state: lexTop, - line: 1, - items: make(chan item, 10), - stack: make([]stateFn, 0, 10), + input: input, + state: lexTop, + items: make(chan item, 10), + stack: make([]stateFn, 0, 10), + line: 1, + tomlNext: tomlNext, } return lx } @@ -125,19 +117,36 @@ func (lx *lexer) current() string { return lx.input[lx.start:lx.pos] } +func (lx lexer) getPos() Position { + p := Position{ + Line: lx.line, + Start: lx.start, + Len: lx.pos - lx.start, + } + if p.Len <= 0 { + p.Len = 1 + } + return p +} + func (lx *lexer) emit(typ itemType) { - lx.items <- item{typ, lx.current(), lx.line} + // Needed for multiline strings ending with an incomplete UTF-8 sequence. + if lx.start > lx.pos { + lx.error(errLexUTF8{lx.input[lx.pos]}) + return + } + lx.items <- item{typ: typ, pos: lx.getPos(), val: lx.current()} lx.start = lx.pos } func (lx *lexer) emitTrim(typ itemType) { - lx.items <- item{typ, strings.TrimSpace(lx.current()), lx.line} + lx.items <- item{typ: typ, pos: lx.getPos(), val: strings.TrimSpace(lx.current())} lx.start = lx.pos } func (lx *lexer) next() (r rune) { if lx.atEOF { - panic("next called after EOF") + panic("BUG in lexer: next called after EOF") } if lx.pos >= len(lx.input) { lx.atEOF = true @@ -147,12 +156,25 @@ func (lx *lexer) next() (r rune) { if lx.input[lx.pos] == '\n' { lx.line++ } + lx.prevWidths[3] = lx.prevWidths[2] lx.prevWidths[2] = lx.prevWidths[1] lx.prevWidths[1] = lx.prevWidths[0] - if lx.nprev < 3 { + if lx.nprev < 4 { lx.nprev++ } + r, w := utf8.DecodeRuneInString(lx.input[lx.pos:]) + if r == utf8.RuneError { + lx.error(errLexUTF8{lx.input[lx.pos]}) + return utf8.RuneError + } + + // Note: don't use peek() here, as this calls next(). + if isControl(r) || (r == '\r' && (len(lx.input)-1 == lx.pos || lx.input[lx.pos+1] != '\n')) { + lx.errorControlChar(r) + return utf8.RuneError + } + lx.prevWidths[0] = w lx.pos += w return r @@ -163,19 +185,21 @@ func (lx *lexer) ignore() { lx.start = lx.pos } -// backup steps back one rune. Can be called only twice between calls to next. +// backup steps back one rune. Can be called 4 times between calls to next. func (lx *lexer) backup() { if lx.atEOF { lx.atEOF = false return } if lx.nprev < 1 { - panic("backed up too far") + panic("BUG in lexer: backed up too far") } w := lx.prevWidths[0] lx.prevWidths[0] = lx.prevWidths[1] lx.prevWidths[1] = lx.prevWidths[2] + lx.prevWidths[2] = lx.prevWidths[3] lx.nprev-- + lx.pos -= w if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' { lx.line-- @@ -211,18 +235,58 @@ func (lx *lexer) skip(pred func(rune) bool) { } } -// errorf stops all lexing by emitting an error and returning `nil`. +// error stops all lexing by emitting an error and returning `nil`. +// // Note that any value that is a character is escaped if it's a special // character (newlines, tabs, etc.). -func (lx *lexer) errorf(format string, values ...interface{}) stateFn { - lx.items <- item{ - itemError, - fmt.Sprintf(format, values...), - lx.line, +func (lx *lexer) error(err error) stateFn { + if lx.atEOF { + return lx.errorPrevLine(err) } + lx.items <- item{typ: itemError, pos: lx.getPos(), err: err} return nil } +// errorfPrevline is like error(), but sets the position to the last column of +// the previous line. +// +// This is so that unexpected EOF or NL errors don't show on a new blank line. +func (lx *lexer) errorPrevLine(err error) stateFn { + pos := lx.getPos() + pos.Line-- + pos.Len = 1 + pos.Start = lx.pos - 1 + lx.items <- item{typ: itemError, pos: pos, err: err} + return nil +} + +// errorPos is like error(), but allows explicitly setting the position. +func (lx *lexer) errorPos(start, length int, err error) stateFn { + pos := lx.getPos() + pos.Start = start + pos.Len = length + lx.items <- item{typ: itemError, pos: pos, err: err} + return nil +} + +// errorf is like error, and creates a new error. +func (lx *lexer) errorf(format string, values ...interface{}) stateFn { + if lx.atEOF { + pos := lx.getPos() + pos.Line-- + pos.Len = 1 + pos.Start = lx.pos - 1 + lx.items <- item{typ: itemError, pos: pos, err: fmt.Errorf(format, values...)} + return nil + } + lx.items <- item{typ: itemError, pos: lx.getPos(), err: fmt.Errorf(format, values...)} + return nil +} + +func (lx *lexer) errorControlChar(cc rune) stateFn { + return lx.errorPos(lx.pos-1, 1, errLexControl{cc}) +} + // lexTop consumes elements at the top level of TOML data. func lexTop(lx *lexer) stateFn { r := lx.next() @@ -230,10 +294,10 @@ func lexTop(lx *lexer) stateFn { return lexSkip(lx, lexTop) } switch r { - case commentStart: + case '#': lx.push(lexTop) return lexCommentStart - case tableStart: + case '[': return lexTableStart case eof: if lx.pos > lx.start { @@ -256,7 +320,7 @@ func lexTop(lx *lexer) stateFn { func lexTopEnd(lx *lexer) stateFn { r := lx.next() switch { - case r == commentStart: + case r == '#': // a comment will read to a newline for us. lx.push(lexTop) return lexCommentStart @@ -269,8 +333,9 @@ func lexTopEnd(lx *lexer) stateFn { lx.emit(itemEOF) return nil } - return lx.errorf("expected a top-level item to end with a newline, "+ - "comment, or EOF, but got %q instead", r) + return lx.errorf( + "expected a top-level item to end with a newline, comment, or EOF, but got %q instead", + r) } // lexTable lexes the beginning of a table. Namely, it makes sure that @@ -279,7 +344,7 @@ func lexTopEnd(lx *lexer) stateFn { // It also handles the case that this is an item in an array of tables. // e.g., '[[name]]'. func lexTableStart(lx *lexer) stateFn { - if lx.peek() == arrayTableStart { + if lx.peek() == '[' { lx.next() lx.emit(itemArrayTableStart) lx.push(lexArrayTableEnd) @@ -296,9 +361,8 @@ func lexTableEnd(lx *lexer) stateFn { } func lexArrayTableEnd(lx *lexer) stateFn { - if r := lx.next(); r != arrayTableEnd { - return lx.errorf("expected end of table array name delimiter %q, "+ - "but got %q instead", arrayTableEnd, r) + if r := lx.next(); r != ']' { + return lx.errorf("expected end of table array name delimiter ']', but got %q instead", r) } lx.emit(itemArrayTableEnd) return lexTopEnd @@ -307,33 +371,20 @@ func lexArrayTableEnd(lx *lexer) stateFn { func lexTableNameStart(lx *lexer) stateFn { lx.skip(isWhitespace) switch r := lx.peek(); { - case r == tableEnd || r == eof: - return lx.errorf("unexpected end of table name " + - "(table names cannot be empty)") - case r == tableSep: - return lx.errorf("unexpected table separator " + - "(table names cannot be empty)") - case r == stringStart || r == rawStringStart: + case r == ']' || r == eof: + return lx.errorf("unexpected end of table name (table names cannot be empty)") + case r == '.': + return lx.errorf("unexpected table separator (table names cannot be empty)") + case r == '"' || r == '\'': lx.ignore() lx.push(lexTableNameEnd) - return lexValue // reuse string lexing + return lexQuotedName default: - return lexBareTableName + lx.push(lexTableNameEnd) + return lexBareName } } -// lexBareTableName lexes the name of a table. It assumes that at least one -// valid character for the table has already been read. -func lexBareTableName(lx *lexer) stateFn { - r := lx.next() - if isBareKeyChar(r) { - return lexBareTableName - } - lx.backup() - lx.emit(itemText) - return lexTableNameEnd -} - // lexTableNameEnd reads the end of a piece of a table name, optionally // consuming whitespace. func lexTableNameEnd(lx *lexer) stateFn { @@ -341,69 +392,107 @@ func lexTableNameEnd(lx *lexer) stateFn { switch r := lx.next(); { case isWhitespace(r): return lexTableNameEnd - case r == tableSep: + case r == '.': lx.ignore() return lexTableNameStart - case r == tableEnd: + case r == ']': return lx.pop() default: - return lx.errorf("expected '.' or ']' to end table name, "+ - "but got %q instead", r) + return lx.errorf("expected '.' or ']' to end table name, but got %q instead", r) } } -// lexKeyStart consumes a key name up until the first non-whitespace character. -// lexKeyStart will ignore whitespace. -func lexKeyStart(lx *lexer) stateFn { - r := lx.peek() +// lexBareName lexes one part of a key or table. +// +// It assumes that at least one valid character for the table has already been +// read. +// +// Lexes only one part, e.g. only 'a' inside 'a.b'. +func lexBareName(lx *lexer) stateFn { + r := lx.next() + if isBareKeyChar(r, lx.tomlNext) { + return lexBareName + } + lx.backup() + lx.emit(itemText) + return lx.pop() +} + +// lexBareName lexes one part of a key or table. +// +// It assumes that at least one valid character for the table has already been +// read. +// +// Lexes only one part, e.g. only '"a"' inside '"a".b'. +func lexQuotedName(lx *lexer) stateFn { + r := lx.next() switch { - case r == keySep: - return lx.errorf("unexpected key separator %q", keySep) - case isWhitespace(r) || isNL(r): - lx.next() - return lexSkip(lx, lexKeyStart) - case r == stringStart || r == rawStringStart: - lx.ignore() - lx.emit(itemKeyStart) - lx.push(lexKeyEnd) - return lexValue // reuse string lexing + case isWhitespace(r): + return lexSkip(lx, lexValue) + case r == '"': + lx.ignore() // ignore the '"' + return lexString + case r == '\'': + lx.ignore() // ignore the "'" + return lexRawString + case r == eof: + return lx.errorf("unexpected EOF; expected value") default: - lx.ignore() - lx.emit(itemKeyStart) - return lexBareKey + return lx.errorf("expected value but found %q instead", r) } } -// lexBareKey consumes the text of a bare key. Assumes that the first character -// (which is not whitespace) has not yet been consumed. -func lexBareKey(lx *lexer) stateFn { - switch r := lx.next(); { - case isBareKeyChar(r): - return lexBareKey - case isWhitespace(r): - lx.backup() - lx.emit(itemText) - return lexKeyEnd - case r == keySep: - lx.backup() - lx.emit(itemText) - return lexKeyEnd +// lexKeyStart consumes all key parts until a '='. +func lexKeyStart(lx *lexer) stateFn { + lx.skip(isWhitespace) + switch r := lx.peek(); { + case r == '=' || r == eof: + return lx.errorf("unexpected '=': key name appears blank") + case r == '.': + return lx.errorf("unexpected '.': keys cannot start with a '.'") + case r == '"' || r == '\'': + lx.ignore() + fallthrough + default: // Bare key + lx.emit(itemKeyStart) + return lexKeyNameStart + } +} + +func lexKeyNameStart(lx *lexer) stateFn { + lx.skip(isWhitespace) + switch r := lx.peek(); { + case r == '=' || r == eof: + return lx.errorf("unexpected '='") + case r == '.': + return lx.errorf("unexpected '.'") + case r == '"' || r == '\'': + lx.ignore() + lx.push(lexKeyEnd) + return lexQuotedName default: - return lx.errorf("bare keys cannot contain %q", r) + lx.push(lexKeyEnd) + return lexBareName } } // lexKeyEnd consumes the end of a key and trims whitespace (up to the key // separator). func lexKeyEnd(lx *lexer) stateFn { + lx.skip(isWhitespace) switch r := lx.next(); { - case r == keySep: - return lexSkip(lx, lexValue) case isWhitespace(r): return lexSkip(lx, lexKeyEnd) + case r == eof: + return lx.errorf("unexpected EOF; expected key separator '='") + case r == '.': + lx.ignore() + return lexKeyNameStart + case r == '=': + lx.emit(itemKeyEnd) + return lexSkip(lx, lexValue) default: - return lx.errorf("expected key separator %q, but got %q instead", - keySep, r) + return lx.errorf("expected '.' or '=', but got %q instead", r) } } @@ -422,17 +511,17 @@ func lexValue(lx *lexer) stateFn { return lexNumberOrDateStart } switch r { - case arrayStart: + case '[': lx.ignore() lx.emit(itemArray) return lexArrayValue - case inlineTableStart: + case '{': lx.ignore() lx.emit(itemInlineTableStart) return lexInlineTableValue - case stringStart: - if lx.accept(stringStart) { - if lx.accept(stringStart) { + case '"': + if lx.accept('"') { + if lx.accept('"') { lx.ignore() // Ignore """ return lexMultilineString } @@ -440,9 +529,9 @@ func lexValue(lx *lexer) stateFn { } lx.ignore() // ignore the '"' return lexString - case rawStringStart: - if lx.accept(rawStringStart) { - if lx.accept(rawStringStart) { + case '\'': + if lx.accept('\'') { + if lx.accept('\'') { lx.ignore() // Ignore """ return lexMultilineRawString } @@ -450,10 +539,15 @@ func lexValue(lx *lexer) stateFn { } lx.ignore() // ignore the "'" return lexRawString - case '+', '-': - return lexNumberStart case '.': // special error case, be kind to users return lx.errorf("floats must start with a digit, not '.'") + case 'i', 'n': + if (lx.accept('n') && lx.accept('f')) || (lx.accept('a') && lx.accept('n')) { + lx.emit(itemFloat) + return lx.pop() + } + case '-', '+': + return lexDecimalNumberStart } if unicode.IsLetter(r) { // Be permissive here; lexBool will give a nice error if the @@ -463,6 +557,9 @@ func lexValue(lx *lexer) stateFn { lx.backup() return lexBool } + if r == eof { + return lx.errorf("unexpected EOF; expected value") + } return lx.errorf("expected value but found %q instead", r) } @@ -473,14 +570,12 @@ func lexArrayValue(lx *lexer) stateFn { switch { case isWhitespace(r) || isNL(r): return lexSkip(lx, lexArrayValue) - case r == commentStart: + case r == '#': lx.push(lexArrayValue) return lexCommentStart - case r == comma: + case r == ',': return lx.errorf("unexpected comma") - case r == arrayEnd: - // NOTE(caleb): The spec isn't clear about whether you can have - // a trailing comma or not, so we'll allow it. + case r == ']': return lexArrayEnd } @@ -493,23 +588,20 @@ func lexArrayValue(lx *lexer) stateFn { // the next value (or the end of the array): it ignores whitespace and newlines // and expects either a ',' or a ']'. func lexArrayValueEnd(lx *lexer) stateFn { - r := lx.next() - switch { + switch r := lx.next(); { case isWhitespace(r) || isNL(r): return lexSkip(lx, lexArrayValueEnd) - case r == commentStart: + case r == '#': lx.push(lexArrayValueEnd) return lexCommentStart - case r == comma: + case r == ',': lx.ignore() return lexArrayValue // move on to the next value - case r == arrayEnd: + case r == ']': return lexArrayEnd + default: + return lx.errorf("expected a comma (',') or array terminator (']'), but got %s", runeOrEOF(r)) } - return lx.errorf( - "expected a comma or array terminator %q, but got %q instead", - arrayEnd, r, - ) } // lexArrayEnd finishes the lexing of an array. @@ -528,13 +620,16 @@ func lexInlineTableValue(lx *lexer) stateFn { case isWhitespace(r): return lexSkip(lx, lexInlineTableValue) case isNL(r): - return lx.errorf("newlines not allowed within inline tables") - case r == commentStart: + if lx.tomlNext { + return lexSkip(lx, lexInlineTableValue) + } + return lx.errorPrevLine(errLexInlineTableNL{}) + case r == '#': lx.push(lexInlineTableValue) return lexCommentStart - case r == comma: + case r == ',': return lx.errorf("unexpected comma") - case r == inlineTableEnd: + case r == '}': return lexInlineTableEnd } lx.backup() @@ -546,23 +641,39 @@ func lexInlineTableValue(lx *lexer) stateFn { // key/value pair and the next pair (or the end of the table): // it ignores whitespace and expects either a ',' or a '}'. func lexInlineTableValueEnd(lx *lexer) stateFn { - r := lx.next() - switch { + switch r := lx.next(); { case isWhitespace(r): return lexSkip(lx, lexInlineTableValueEnd) case isNL(r): - return lx.errorf("newlines not allowed within inline tables") - case r == commentStart: + if lx.tomlNext { + return lexSkip(lx, lexInlineTableValueEnd) + } + return lx.errorPrevLine(errLexInlineTableNL{}) + case r == '#': lx.push(lexInlineTableValueEnd) return lexCommentStart - case r == comma: + case r == ',': lx.ignore() + lx.skip(isWhitespace) + if lx.peek() == '}' { + if lx.tomlNext { + return lexInlineTableValueEnd + } + return lx.errorf("trailing comma not allowed in inline tables") + } return lexInlineTableValue - case r == inlineTableEnd: + case r == '}': return lexInlineTableEnd + default: + return lx.errorf("expected a comma or an inline table terminator '}', but got %s instead", runeOrEOF(r)) } - return lx.errorf("expected a comma or an inline table terminator %q, "+ - "but got %q instead", inlineTableEnd, r) +} + +func runeOrEOF(r rune) string { + if r == eof { + return "end of file" + } + return "'" + string(r) + "'" } // lexInlineTableEnd finishes the lexing of an inline table. @@ -579,13 +690,13 @@ func lexString(lx *lexer) stateFn { r := lx.next() switch { case r == eof: - return lx.errorf("unexpected EOF") + return lx.errorf(`unexpected EOF; expected '"'`) case isNL(r): - return lx.errorf("strings cannot contain newlines") + return lx.errorPrevLine(errLexStringNL{}) case r == '\\': lx.push(lexString) return lexStringEscape - case r == stringEnd: + case r == '"': lx.backup() lx.emit(itemString) lx.next() @@ -598,19 +709,47 @@ func lexString(lx *lexer) stateFn { // lexMultilineString consumes the inner contents of a string. It assumes that // the beginning '"""' has already been consumed and ignored. func lexMultilineString(lx *lexer) stateFn { - switch lx.next() { + r := lx.next() + switch r { + default: + return lexMultilineString case eof: - return lx.errorf("unexpected EOF") + return lx.errorf(`unexpected EOF; expected '"""'`) case '\\': return lexMultilineStringEscape - case stringEnd: - if lx.accept(stringEnd) { - if lx.accept(stringEnd) { - lx.backup() + case '"': + /// Found " → try to read two more "". + if lx.accept('"') { + if lx.accept('"') { + /// Peek ahead: the string can contain " and "", including at the + /// end: """str""""" + /// 6 or more at the end, however, is an error. + if lx.peek() == '"' { + /// Check if we already lexed 5 's; if so we have 6 now, and + /// that's just too many man! + /// + /// Second check is for the edge case: + /// + /// two quotes allowed. + /// vv + /// """lol \"""""" + /// ^^ ^^^---- closing three + /// escaped + /// + /// But ugly, but it works + if strings.HasSuffix(lx.current(), `"""""`) && !strings.HasSuffix(lx.current(), `\"""""`) { + return lx.errorf(`unexpected '""""""'`) + } + lx.backup() + lx.backup() + return lexMultilineString + } + + lx.backup() /// backup: don't include the """ in the item. lx.backup() lx.backup() lx.emit(itemMultilineString) - lx.next() + lx.next() /// Read over ''' again and discard it. lx.next() lx.next() lx.ignore() @@ -618,8 +757,8 @@ func lexMultilineString(lx *lexer) stateFn { } lx.backup() } + return lexMultilineString } - return lexMultilineString } // lexRawString consumes a raw string. Nothing can be escaped in such a string. @@ -627,35 +766,54 @@ func lexMultilineString(lx *lexer) stateFn { func lexRawString(lx *lexer) stateFn { r := lx.next() switch { + default: + return lexRawString case r == eof: - return lx.errorf("unexpected EOF") + return lx.errorf(`unexpected EOF; expected "'"`) case isNL(r): - return lx.errorf("strings cannot contain newlines") - case r == rawStringEnd: + return lx.errorPrevLine(errLexStringNL{}) + case r == '\'': lx.backup() lx.emit(itemRawString) lx.next() lx.ignore() return lx.pop() } - return lexRawString } -// lexMultilineRawString consumes a raw string. Nothing can be escaped in such -// a string. It assumes that the beginning "'''" has already been consumed and +// lexMultilineRawString consumes a raw string. Nothing can be escaped in such a +// string. It assumes that the beginning triple-' has already been consumed and // ignored. func lexMultilineRawString(lx *lexer) stateFn { - switch lx.next() { + r := lx.next() + switch r { + default: + return lexMultilineRawString case eof: - return lx.errorf("unexpected EOF") - case rawStringEnd: - if lx.accept(rawStringEnd) { - if lx.accept(rawStringEnd) { - lx.backup() + return lx.errorf(`unexpected EOF; expected "'''"`) + case '\'': + /// Found ' → try to read two more ''. + if lx.accept('\'') { + if lx.accept('\'') { + /// Peek ahead: the string can contain ' and '', including at the + /// end: '''str''''' + /// 6 or more at the end, however, is an error. + if lx.peek() == '\'' { + /// Check if we already lexed 5 's; if so we have 6 now, and + /// that's just too many man! + if strings.HasSuffix(lx.current(), "'''''") { + return lx.errorf(`unexpected "''''''"`) + } + lx.backup() + lx.backup() + return lexMultilineRawString + } + + lx.backup() /// backup: don't include the ''' in the item. lx.backup() lx.backup() lx.emit(itemRawMultilineString) - lx.next() + lx.next() /// Read over ''' again and discard it. lx.next() lx.next() lx.ignore() @@ -663,15 +821,14 @@ func lexMultilineRawString(lx *lexer) stateFn { } lx.backup() } + return lexMultilineRawString } - return lexMultilineRawString } // lexMultilineStringEscape consumes an escaped character. It assumes that the // preceding '\\' has already been consumed. func lexMultilineStringEscape(lx *lexer) stateFn { - // Handle the special case first: - if isNL(lx.next()) { + if isNL(lx.next()) { /// \ escaping newline. return lexMultilineString } lx.backup() @@ -682,6 +839,11 @@ func lexMultilineStringEscape(lx *lexer) stateFn { func lexStringEscape(lx *lexer) stateFn { r := lx.next() switch r { + case 'e': + if !lx.tomlNext { + return lx.error(errLexEscape{r}) + } + fallthrough case 'b': fallthrough case 't': @@ -694,16 +856,36 @@ func lexStringEscape(lx *lexer) stateFn { fallthrough case '"': fallthrough + case ' ', '\t': + // Inside """ .. """ strings you can use \ to escape newlines, and any + // amount of whitespace can be between the \ and \n. + fallthrough case '\\': return lx.pop() + case 'x': + if !lx.tomlNext { + return lx.error(errLexEscape{r}) + } + return lexHexEscape case 'u': return lexShortUnicodeEscape case 'U': return lexLongUnicodeEscape } - return lx.errorf("invalid escape character %q; only the following "+ - "escape characters are allowed: "+ - `\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX`, r) + return lx.error(errLexEscape{r}) +} + +func lexHexEscape(lx *lexer) stateFn { + var r rune + for i := 0; i < 2; i++ { + r = lx.next() + if !isHexadecimal(r) { + return lx.errorf( + `expected two hexadecimal digits after '\x', but got %q instead`, + lx.current()) + } + } + return lx.pop() } func lexShortUnicodeEscape(lx *lexer) stateFn { @@ -711,8 +893,9 @@ func lexShortUnicodeEscape(lx *lexer) stateFn { for i := 0; i < 4; i++ { r = lx.next() if !isHexadecimal(r) { - return lx.errorf(`expected four hexadecimal digits after '\u', `+ - "but got %q instead", lx.current()) + return lx.errorf( + `expected four hexadecimal digits after '\u', but got %q instead`, + lx.current()) } } return lx.pop() @@ -723,28 +906,33 @@ func lexLongUnicodeEscape(lx *lexer) stateFn { for i := 0; i < 8; i++ { r = lx.next() if !isHexadecimal(r) { - return lx.errorf(`expected eight hexadecimal digits after '\U', `+ - "but got %q instead", lx.current()) + return lx.errorf( + `expected eight hexadecimal digits after '\U', but got %q instead`, + lx.current()) } } return lx.pop() } -// lexNumberOrDateStart consumes either an integer, a float, or datetime. +// lexNumberOrDateStart processes the first character of a value which begins +// with a digit. It exists to catch values starting with '0', so that +// lexBaseNumberOrDate can differentiate base prefixed integers from other +// types. func lexNumberOrDateStart(lx *lexer) stateFn { r := lx.next() - if isDigit(r) { - return lexNumberOrDate - } switch r { - case '_': - return lexNumber - case 'e', 'E': - return lexFloat - case '.': - return lx.errorf("floats must start with a digit, not '.'") + case '0': + return lexBaseNumberOrDate } - return lx.errorf("expected a digit but got %q", r) + + if !isDigit(r) { + // The only way to reach this state is if the value starts + // with a digit, so specifically treat anything else as an + // error. + return lx.errorf("expected a digit but got %q", r) + } + + return lexNumberOrDate } // lexNumberOrDate consumes either an integer, float or datetime. @@ -754,10 +942,10 @@ func lexNumberOrDate(lx *lexer) stateFn { return lexNumberOrDate } switch r { - case '-': + case '-', ':': return lexDatetime case '_': - return lexNumber + return lexDecimalNumber case '.', 'e', 'E': return lexFloat } @@ -775,41 +963,156 @@ func lexDatetime(lx *lexer) stateFn { return lexDatetime } switch r { - case '-', 'T', ':', '.', 'Z', '+': + case '-', ':', 'T', 't', ' ', '.', 'Z', 'z', '+': return lexDatetime } lx.backup() - lx.emit(itemDatetime) + lx.emitTrim(itemDatetime) return lx.pop() } -// lexNumberStart consumes either an integer or a float. It assumes that a sign -// has already been read, but that *no* digits have been consumed. -// lexNumberStart will move to the appropriate integer or float states. -func lexNumberStart(lx *lexer) stateFn { - // We MUST see a digit. Even floats have to start with a digit. +// lexHexInteger consumes a hexadecimal integer after seeing the '0x' prefix. +func lexHexInteger(lx *lexer) stateFn { r := lx.next() - if !isDigit(r) { - if r == '.' { - return lx.errorf("floats must start with a digit, not '.'") - } - return lx.errorf("expected a digit but got %q", r) - } - return lexNumber -} - -// lexNumber consumes an integer or a float after seeing the first digit. -func lexNumber(lx *lexer) stateFn { - r := lx.next() - if isDigit(r) { - return lexNumber + if isHexadecimal(r) { + return lexHexInteger } switch r { case '_': - return lexNumber + return lexHexInteger + } + + lx.backup() + lx.emit(itemInteger) + return lx.pop() +} + +// lexOctalInteger consumes an octal integer after seeing the '0o' prefix. +func lexOctalInteger(lx *lexer) stateFn { + r := lx.next() + if isOctal(r) { + return lexOctalInteger + } + switch r { + case '_': + return lexOctalInteger + } + + lx.backup() + lx.emit(itemInteger) + return lx.pop() +} + +// lexBinaryInteger consumes a binary integer after seeing the '0b' prefix. +func lexBinaryInteger(lx *lexer) stateFn { + r := lx.next() + if isBinary(r) { + return lexBinaryInteger + } + switch r { + case '_': + return lexBinaryInteger + } + + lx.backup() + lx.emit(itemInteger) + return lx.pop() +} + +// lexDecimalNumber consumes a decimal float or integer. +func lexDecimalNumber(lx *lexer) stateFn { + r := lx.next() + if isDigit(r) { + return lexDecimalNumber + } + switch r { case '.', 'e', 'E': return lexFloat + case '_': + return lexDecimalNumber + } + + lx.backup() + lx.emit(itemInteger) + return lx.pop() +} + +// lexDecimalNumber consumes the first digit of a number beginning with a sign. +// It assumes the sign has already been consumed. Values which start with a sign +// are only allowed to be decimal integers or floats. +// +// The special "nan" and "inf" values are also recognized. +func lexDecimalNumberStart(lx *lexer) stateFn { + r := lx.next() + + // Special error cases to give users better error messages + switch r { + case 'i': + if !lx.accept('n') || !lx.accept('f') { + return lx.errorf("invalid float: '%s'", lx.current()) + } + lx.emit(itemFloat) + return lx.pop() + case 'n': + if !lx.accept('a') || !lx.accept('n') { + return lx.errorf("invalid float: '%s'", lx.current()) + } + lx.emit(itemFloat) + return lx.pop() + case '0': + p := lx.peek() + switch p { + case 'b', 'o', 'x': + return lx.errorf("cannot use sign with non-decimal numbers: '%s%c'", lx.current(), p) + } + case '.': + return lx.errorf("floats must start with a digit, not '.'") + } + + if isDigit(r) { + return lexDecimalNumber + } + + return lx.errorf("expected a digit but got %q", r) +} + +// lexBaseNumberOrDate differentiates between the possible values which +// start with '0'. It assumes that before reaching this state, the initial '0' +// has been consumed. +func lexBaseNumberOrDate(lx *lexer) stateFn { + r := lx.next() + // Note: All datetimes start with at least two digits, so we don't + // handle date characters (':', '-', etc.) here. + if isDigit(r) { + return lexNumberOrDate + } + switch r { + case '_': + // Can only be decimal, because there can't be an underscore + // between the '0' and the base designator, and dates can't + // contain underscores. + return lexDecimalNumber + case '.', 'e', 'E': + return lexFloat + case 'b': + r = lx.peek() + if !isBinary(r) { + lx.errorf("not a binary number: '%s%c'", lx.current(), r) + } + return lexBinaryInteger + case 'o': + r = lx.peek() + if !isOctal(r) { + lx.errorf("not an octal number: '%s%c'", lx.current(), r) + } + return lexOctalInteger + case 'x': + r = lx.peek() + if !isHexadecimal(r) { + lx.errorf("not a hexidecimal number: '%s%c'", lx.current(), r) + } + return lexHexInteger } lx.backup() @@ -867,49 +1170,31 @@ func lexCommentStart(lx *lexer) stateFn { // It will consume *up to* the first newline character, and pass control // back to the last state on the stack. func lexComment(lx *lexer) stateFn { - r := lx.peek() - if isNL(r) || r == eof { + switch r := lx.next(); { + case isNL(r) || r == eof: + lx.backup() lx.emit(itemText) return lx.pop() + default: + return lexComment } - lx.next() - return lexComment } // lexSkip ignores all slurped input and moves on to the next state. func lexSkip(lx *lexer, nextState stateFn) stateFn { - return func(lx *lexer) stateFn { - lx.ignore() - return nextState + lx.ignore() + return nextState +} + +func (s stateFn) String() string { + name := runtime.FuncForPC(reflect.ValueOf(s).Pointer()).Name() + if i := strings.LastIndexByte(name, '.'); i > -1 { + name = name[i+1:] } -} - -// isWhitespace returns true if `r` is a whitespace character according -// to the spec. -func isWhitespace(r rune) bool { - return r == '\t' || r == ' ' -} - -func isNL(r rune) bool { - return r == '\n' || r == '\r' -} - -func isDigit(r rune) bool { - return r >= '0' && r <= '9' -} - -func isHexadecimal(r rune) bool { - return (r >= '0' && r <= '9') || - (r >= 'a' && r <= 'f') || - (r >= 'A' && r <= 'F') -} - -func isBareKeyChar(r rune) bool { - return (r >= 'A' && r <= 'Z') || - (r >= 'a' && r <= 'z') || - (r >= '0' && r <= '9') || - r == '_' || - r == '-' + if s == nil { + name = "" + } + return name + "()" } func (itype itemType) String() string { @@ -938,12 +1223,18 @@ func (itype itemType) String() string { return "TableEnd" case itemKeyStart: return "KeyStart" + case itemKeyEnd: + return "KeyEnd" case itemArray: return "Array" case itemArrayEnd: return "ArrayEnd" case itemCommentStart: return "CommentStart" + case itemInlineTableStart: + return "InlineTableStart" + case itemInlineTableEnd: + return "InlineTableEnd" } panic(fmt.Sprintf("BUG: Unknown type '%d'.", int(itype))) } @@ -951,3 +1242,42 @@ func (itype itemType) String() string { func (item item) String() string { return fmt.Sprintf("(%s, %s)", item.typ.String(), item.val) } + +func isWhitespace(r rune) bool { return r == '\t' || r == ' ' } +func isNL(r rune) bool { return r == '\n' || r == '\r' } +func isControl(r rune) bool { // Control characters except \t, \r, \n + switch r { + case '\t', '\r', '\n': + return false + default: + return (r >= 0x00 && r <= 0x1f) || r == 0x7f + } +} +func isDigit(r rune) bool { return r >= '0' && r <= '9' } +func isBinary(r rune) bool { return r == '0' || r == '1' } +func isOctal(r rune) bool { return r >= '0' && r <= '7' } +func isHexadecimal(r rune) bool { + return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') +} + +func isBareKeyChar(r rune, tomlNext bool) bool { + if tomlNext { + return (r >= 'A' && r <= 'Z') || + (r >= 'a' && r <= 'z') || + (r >= '0' && r <= '9') || + r == '_' || r == '-' || + r == 0xb2 || r == 0xb3 || r == 0xb9 || (r >= 0xbc && r <= 0xbe) || + (r >= 0xc0 && r <= 0xd6) || (r >= 0xd8 && r <= 0xf6) || (r >= 0xf8 && r <= 0x037d) || + (r >= 0x037f && r <= 0x1fff) || + (r >= 0x200c && r <= 0x200d) || (r >= 0x203f && r <= 0x2040) || + (r >= 0x2070 && r <= 0x218f) || (r >= 0x2460 && r <= 0x24ff) || + (r >= 0x2c00 && r <= 0x2fef) || (r >= 0x3001 && r <= 0xd7ff) || + (r >= 0xf900 && r <= 0xfdcf) || (r >= 0xfdf0 && r <= 0xfffd) || + (r >= 0x10000 && r <= 0xeffff) + } + + return (r >= 'A' && r <= 'Z') || + (r >= 'a' && r <= 'z') || + (r >= '0' && r <= '9') || + r == '_' || r == '-' +} diff --git a/vendor/github.com/BurntSushi/toml/decode_meta.go b/vendor/github.com/BurntSushi/toml/meta.go similarity index 59% rename from vendor/github.com/BurntSushi/toml/decode_meta.go rename to vendor/github.com/BurntSushi/toml/meta.go index b9914a67..2e78b24e 100644 --- a/vendor/github.com/BurntSushi/toml/decode_meta.go +++ b/vendor/github.com/BurntSushi/toml/meta.go @@ -1,33 +1,40 @@ package toml -import "strings" +import ( + "strings" +) -// MetaData allows access to meta information about TOML data that may not -// be inferrable via reflection. In particular, whether a key has been defined -// and the TOML type of a key. +// MetaData allows access to meta information about TOML data that's not +// accessible otherwise. +// +// It allows checking if a key is defined in the TOML data, whether any keys +// were undecoded, and the TOML type of a key. type MetaData struct { - mapping map[string]interface{} - types map[string]tomlType - keys []Key - decoded map[string]bool context Key // Used only during decoding. + + keyInfo map[string]keyInfo + mapping map[string]interface{} + keys []Key + decoded map[string]struct{} + data []byte // Input file; for errors. } -// IsDefined returns true if the key given exists in the TOML data. The key -// should be specified hierarchially. e.g., +// IsDefined reports if the key exists in the TOML data. // -// // access the TOML key 'a.b.c' -// IsDefined("a", "b", "c") +// The key should be specified hierarchically, for example to access the TOML +// key "a.b.c" you would use IsDefined("a", "b", "c"). Keys are case sensitive. // -// IsDefined will return false if an empty key given. Keys are case sensitive. +// Returns false for an empty key. func (md *MetaData) IsDefined(key ...string) bool { if len(key) == 0 { return false } - var hash map[string]interface{} - var ok bool - var hashOrVal interface{} = md.mapping + var ( + hash map[string]interface{} + ok bool + hashOrVal interface{} = md.mapping + ) for _, k := range key { if hash, ok = hashOrVal.(map[string]interface{}); !ok { return false @@ -41,58 +48,20 @@ func (md *MetaData) IsDefined(key ...string) bool { // Type returns a string representation of the type of the key specified. // -// Type will return the empty string if given an empty key or a key that -// does not exist. Keys are case sensitive. +// Type will return the empty string if given an empty key or a key that does +// not exist. Keys are case sensitive. func (md *MetaData) Type(key ...string) string { - fullkey := strings.Join(key, ".") - if typ, ok := md.types[fullkey]; ok { - return typ.typeString() + if ki, ok := md.keyInfo[Key(key).String()]; ok { + return ki.tomlType.typeString() } return "" } -// Key is the type of any TOML key, including key groups. Use (MetaData).Keys -// to get values of this type. -type Key []string - -func (k Key) String() string { - return strings.Join(k, ".") -} - -func (k Key) maybeQuotedAll() string { - var ss []string - for i := range k { - ss = append(ss, k.maybeQuoted(i)) - } - return strings.Join(ss, ".") -} - -func (k Key) maybeQuoted(i int) string { - quote := false - for _, c := range k[i] { - if !isBareKeyChar(c) { - quote = true - break - } - } - if quote { - return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\"" - } - return k[i] -} - -func (k Key) add(piece string) Key { - newKey := make(Key, len(k)+1) - copy(newKey, k) - newKey[len(k)] = piece - return newKey -} - // Keys returns a slice of every key in the TOML data, including key groups. -// Each key is itself a slice, where the first element is the top of the -// hierarchy and the last is the most specific. // -// The list will have the same order as the keys appeared in the TOML data. +// Each key is itself a slice, where the first element is the top of the +// hierarchy and the last is the most specific. The list will have the same +// order as the keys appeared in the TOML data. // // All keys returned are non-empty. func (md *MetaData) Keys() []Key { @@ -102,7 +71,7 @@ func (md *MetaData) Keys() []Key { // Undecoded returns all keys that have not been decoded in the order in which // they appear in the original TOML document. // -// This includes keys that haven't been decoded because of a Primitive value. +// This includes keys that haven't been decoded because of a [Primitive] value. // Once the Primitive value is decoded, the keys will be considered decoded. // // Also note that decoding into an empty interface will result in no decoding, @@ -113,9 +82,40 @@ func (md *MetaData) Keys() []Key { func (md *MetaData) Undecoded() []Key { undecoded := make([]Key, 0, len(md.keys)) for _, key := range md.keys { - if !md.decoded[key.String()] { + if _, ok := md.decoded[key.String()]; !ok { undecoded = append(undecoded, key) } } return undecoded } + +// Key represents any TOML key, including key groups. Use [MetaData.Keys] to get +// values of this type. +type Key []string + +func (k Key) String() string { + ss := make([]string, len(k)) + for i := range k { + ss[i] = k.maybeQuoted(i) + } + return strings.Join(ss, ".") +} + +func (k Key) maybeQuoted(i int) string { + if k[i] == "" { + return `""` + } + for _, c := range k[i] { + if !isBareKeyChar(c, false) { + return `"` + dblQuotedReplacer.Replace(k[i]) + `"` + } + } + return k[i] +} + +func (k Key) add(piece string) Key { + newKey := make(Key, len(k)+1) + copy(newKey, k) + newKey[len(k)] = piece + return newKey +} diff --git a/vendor/github.com/BurntSushi/toml/parse.go b/vendor/github.com/BurntSushi/toml/parse.go index 50869ef9..9c191536 100644 --- a/vendor/github.com/BurntSushi/toml/parse.go +++ b/vendor/github.com/BurntSushi/toml/parse.go @@ -2,57 +2,80 @@ package toml import ( "fmt" + "os" "strconv" "strings" "time" - "unicode" "unicode/utf8" + + "github.com/BurntSushi/toml/internal" ) type parser struct { - mapping map[string]interface{} - types map[string]tomlType - lx *lexer + lx *lexer + context Key // Full key for the current hash in scope. + currentKey string // Base key name for everything except hashes. + pos Position // Current position in the TOML file. + tomlNext bool - // A list of keys in the order that they appear in the TOML data. - ordered []Key + ordered []Key // List of keys in the order that they appear in the TOML data. - // the full key for the current hash in scope - context Key - - // the base key name for everything except hashes - currentKey string - - // rough approximation of line number - approxLine int - - // A map of 'key.group.names' to whether they were created implicitly. - implicits map[string]bool + keyInfo map[string]keyInfo // Map keyname → info about the TOML key. + mapping map[string]interface{} // Map keyname → key value. + implicits map[string]struct{} // Record implicit keys (e.g. "key.group.names"). } -type parseError string - -func (pe parseError) Error() string { - return string(pe) +type keyInfo struct { + pos Position + tomlType tomlType } func parse(data string) (p *parser, err error) { + _, tomlNext := os.LookupEnv("BURNTSUSHI_TOML_110") + defer func() { if r := recover(); r != nil { - var ok bool - if err, ok = r.(parseError); ok { + if pErr, ok := r.(ParseError); ok { + pErr.input = data + err = pErr return } panic(r) } }() + // Read over BOM; do this here as the lexer calls utf8.DecodeRuneInString() + // which mangles stuff. UTF-16 BOM isn't strictly valid, but some tools add + // it anyway. + if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") { // UTF-16 + data = data[2:] + } else if strings.HasPrefix(data, "\xef\xbb\xbf") { // UTF-8 + data = data[3:] + } + + // Examine first few bytes for NULL bytes; this probably means it's a UTF-16 + // file (second byte in surrogate pair being NULL). Again, do this here to + // avoid having to deal with UTF-8/16 stuff in the lexer. + ex := 6 + if len(data) < 6 { + ex = len(data) + } + if i := strings.IndexRune(data[:ex], 0); i > -1 { + return nil, ParseError{ + Message: "files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8", + Position: Position{Line: 1, Start: i, Len: 1}, + Line: 1, + input: data, + } + } + p = &parser{ + keyInfo: make(map[string]keyInfo), mapping: make(map[string]interface{}), - types: make(map[string]tomlType), - lx: lex(data), + lx: lex(data, tomlNext), ordered: make([]Key, 0), - implicits: make(map[string]bool), + implicits: make(map[string]struct{}), + tomlNext: tomlNext, } for { item := p.next() @@ -65,20 +88,57 @@ func parse(data string) (p *parser, err error) { return p, nil } +func (p *parser) panicErr(it item, err error) { + panic(ParseError{ + err: err, + Position: it.pos, + Line: it.pos.Len, + LastKey: p.current(), + }) +} + +func (p *parser) panicItemf(it item, format string, v ...interface{}) { + panic(ParseError{ + Message: fmt.Sprintf(format, v...), + Position: it.pos, + Line: it.pos.Len, + LastKey: p.current(), + }) +} + func (p *parser) panicf(format string, v ...interface{}) { - msg := fmt.Sprintf("Near line %d (last key parsed '%s'): %s", - p.approxLine, p.current(), fmt.Sprintf(format, v...)) - panic(parseError(msg)) + panic(ParseError{ + Message: fmt.Sprintf(format, v...), + Position: p.pos, + Line: p.pos.Line, + LastKey: p.current(), + }) } func (p *parser) next() item { it := p.lx.nextItem() + //fmt.Printf("ITEM %-18s line %-3d │ %q\n", it.typ, it.pos.Line, it.val) if it.typ == itemError { - p.panicf("%s", it.val) + if it.err != nil { + panic(ParseError{ + Position: it.pos, + Line: it.pos.Line, + LastKey: p.current(), + err: it.err, + }) + } + + p.panicItemf(it, "%s", it.val) } return it } +func (p *parser) nextPos() item { + it := p.next() + p.pos = it.pos + return it +} + func (p *parser) bug(format string, v ...interface{}) { panic(fmt.Sprintf("BUG: "+format+"\n\n", v...)) } @@ -97,44 +157,60 @@ func (p *parser) assertEqual(expected, got itemType) { func (p *parser) topLevel(item item) { switch item.typ { - case itemCommentStart: - p.approxLine = item.line + case itemCommentStart: // # .. p.expect(itemText) - case itemTableStart: - kg := p.next() - p.approxLine = kg.line + case itemTableStart: // [ .. ] + name := p.nextPos() var key Key - for ; kg.typ != itemTableEnd && kg.typ != itemEOF; kg = p.next() { - key = append(key, p.keyString(kg)) + for ; name.typ != itemTableEnd && name.typ != itemEOF; name = p.next() { + key = append(key, p.keyString(name)) } - p.assertEqual(itemTableEnd, kg.typ) + p.assertEqual(itemTableEnd, name.typ) - p.establishContext(key, false) - p.setType("", tomlHash) + p.addContext(key, false) + p.setType("", tomlHash, item.pos) p.ordered = append(p.ordered, key) - case itemArrayTableStart: - kg := p.next() - p.approxLine = kg.line + case itemArrayTableStart: // [[ .. ]] + name := p.nextPos() var key Key - for ; kg.typ != itemArrayTableEnd && kg.typ != itemEOF; kg = p.next() { - key = append(key, p.keyString(kg)) + for ; name.typ != itemArrayTableEnd && name.typ != itemEOF; name = p.next() { + key = append(key, p.keyString(name)) } - p.assertEqual(itemArrayTableEnd, kg.typ) + p.assertEqual(itemArrayTableEnd, name.typ) - p.establishContext(key, true) - p.setType("", tomlArrayHash) + p.addContext(key, true) + p.setType("", tomlArrayHash, item.pos) p.ordered = append(p.ordered, key) - case itemKeyStart: - kname := p.next() - p.approxLine = kname.line - p.currentKey = p.keyString(kname) + case itemKeyStart: // key = .. + outerContext := p.context + /// Read all the key parts (e.g. 'a' and 'b' in 'a.b') + k := p.nextPos() + var key Key + for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() { + key = append(key, p.keyString(k)) + } + p.assertEqual(itemKeyEnd, k.typ) - val, typ := p.value(p.next()) - p.setValue(p.currentKey, val) - p.setType(p.currentKey, typ) + /// The current key is the last part. + p.currentKey = key[len(key)-1] + + /// All the other parts (if any) are the context; need to set each part + /// as implicit. + context := key[:len(key)-1] + for i := range context { + p.addImplicitContext(append(p.context, context[i:i+1]...)) + } p.ordered = append(p.ordered, p.context.add(p.currentKey)) + + /// Set value. + vItem := p.next() + val, typ := p.value(vItem, false) + p.set(p.currentKey, val, typ, vItem.pos) + + /// Remove the context we added (preserving any context from [tbl] lines). + p.context = outerContext p.currentKey = "" default: p.bug("Unexpected type at top level: %s", item.typ) @@ -148,180 +224,271 @@ func (p *parser) keyString(it item) string { return it.val case itemString, itemMultilineString, itemRawString, itemRawMultilineString: - s, _ := p.value(it) + s, _ := p.value(it, false) return s.(string) default: p.bug("Unexpected key type: %s", it.typ) - panic("unreachable") } + panic("unreachable") } +var datetimeRepl = strings.NewReplacer( + "z", "Z", + "t", "T", + " ", "T") + // value translates an expected value from the lexer into a Go value wrapped // as an empty interface. -func (p *parser) value(it item) (interface{}, tomlType) { +func (p *parser) value(it item, parentIsArray bool) (interface{}, tomlType) { switch it.typ { case itemString: - return p.replaceEscapes(it.val), p.typeOfPrimitive(it) + return p.replaceEscapes(it, it.val), p.typeOfPrimitive(it) case itemMultilineString: - trimmed := stripFirstNewline(stripEscapedWhitespace(it.val)) - return p.replaceEscapes(trimmed), p.typeOfPrimitive(it) + return p.replaceEscapes(it, p.stripEscapedNewlines(stripFirstNewline(it.val))), p.typeOfPrimitive(it) case itemRawString: return it.val, p.typeOfPrimitive(it) case itemRawMultilineString: return stripFirstNewline(it.val), p.typeOfPrimitive(it) + case itemInteger: + return p.valueInteger(it) + case itemFloat: + return p.valueFloat(it) case itemBool: switch it.val { case "true": return true, p.typeOfPrimitive(it) case "false": return false, p.typeOfPrimitive(it) + default: + p.bug("Expected boolean value, but got '%s'.", it.val) } - p.bug("Expected boolean value, but got '%s'.", it.val) - case itemInteger: - if !numUnderscoresOK(it.val) { - p.panicf("Invalid integer %q: underscores must be surrounded by digits", - it.val) - } - val := strings.Replace(it.val, "_", "", -1) - num, err := strconv.ParseInt(val, 10, 64) - if err != nil { - // Distinguish integer values. Normally, it'd be a bug if the lexer - // provides an invalid integer, but it's possible that the number is - // out of range of valid values (which the lexer cannot determine). - // So mark the former as a bug but the latter as a legitimate user - // error. - if e, ok := err.(*strconv.NumError); ok && - e.Err == strconv.ErrRange { - - p.panicf("Integer '%s' is out of the range of 64-bit "+ - "signed integers.", it.val) - } else { - p.bug("Expected integer value, but got '%s'.", it.val) - } - } - return num, p.typeOfPrimitive(it) - case itemFloat: - parts := strings.FieldsFunc(it.val, func(r rune) bool { - switch r { - case '.', 'e', 'E': - return true - } - return false - }) - for _, part := range parts { - if !numUnderscoresOK(part) { - p.panicf("Invalid float %q: underscores must be "+ - "surrounded by digits", it.val) - } - } - if !numPeriodsOK(it.val) { - // As a special case, numbers like '123.' or '1.e2', - // which are valid as far as Go/strconv are concerned, - // must be rejected because TOML says that a fractional - // part consists of '.' followed by 1+ digits. - p.panicf("Invalid float %q: '.' must be followed "+ - "by one or more digits", it.val) - } - val := strings.Replace(it.val, "_", "", -1) - num, err := strconv.ParseFloat(val, 64) - if err != nil { - if e, ok := err.(*strconv.NumError); ok && - e.Err == strconv.ErrRange { - - p.panicf("Float '%s' is out of the range of 64-bit "+ - "IEEE-754 floating-point numbers.", it.val) - } else { - p.panicf("Invalid float value: %q", it.val) - } - } - return num, p.typeOfPrimitive(it) case itemDatetime: - var t time.Time - var ok bool - var err error - for _, format := range []string{ - "2006-01-02T15:04:05Z07:00", - "2006-01-02T15:04:05", - "2006-01-02", - } { - t, err = time.ParseInLocation(format, it.val, time.Local) - if err == nil { - ok = true - break - } - } - if !ok { - p.panicf("Invalid TOML Datetime: %q.", it.val) - } - return t, p.typeOfPrimitive(it) + return p.valueDatetime(it) case itemArray: - array := make([]interface{}, 0) - types := make([]tomlType, 0) - - for it = p.next(); it.typ != itemArrayEnd; it = p.next() { - if it.typ == itemCommentStart { - p.expect(itemText) - continue - } - - val, typ := p.value(it) - array = append(array, val) - types = append(types, typ) - } - return array, p.typeOfArray(types) + return p.valueArray(it) case itemInlineTableStart: - var ( - hash = make(map[string]interface{}) - outerContext = p.context - outerKey = p.currentKey - ) - - p.context = append(p.context, p.currentKey) - p.currentKey = "" - for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() { - if it.typ != itemKeyStart { - p.bug("Expected key start but instead found %q, around line %d", - it.val, p.approxLine) - } - if it.typ == itemCommentStart { - p.expect(itemText) - continue - } - - // retrieve key - k := p.next() - p.approxLine = k.line - kname := p.keyString(k) - - // retrieve value - p.currentKey = kname - val, typ := p.value(p.next()) - // make sure we keep metadata up to date - p.setType(kname, typ) - p.ordered = append(p.ordered, p.context.add(p.currentKey)) - hash[kname] = val - } - p.context = outerContext - p.currentKey = outerKey - return hash, tomlHash + return p.valueInlineTable(it, parentIsArray) + default: + p.bug("Unexpected value type: %s", it.typ) } - p.bug("Unexpected value type: %s", it.typ) panic("unreachable") } +func (p *parser) valueInteger(it item) (interface{}, tomlType) { + if !numUnderscoresOK(it.val) { + p.panicItemf(it, "Invalid integer %q: underscores must be surrounded by digits", it.val) + } + if numHasLeadingZero(it.val) { + p.panicItemf(it, "Invalid integer %q: cannot have leading zeroes", it.val) + } + + num, err := strconv.ParseInt(it.val, 0, 64) + if err != nil { + // Distinguish integer values. Normally, it'd be a bug if the lexer + // provides an invalid integer, but it's possible that the number is + // out of range of valid values (which the lexer cannot determine). + // So mark the former as a bug but the latter as a legitimate user + // error. + if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { + p.panicErr(it, errParseRange{i: it.val, size: "int64"}) + } else { + p.bug("Expected integer value, but got '%s'.", it.val) + } + } + return num, p.typeOfPrimitive(it) +} + +func (p *parser) valueFloat(it item) (interface{}, tomlType) { + parts := strings.FieldsFunc(it.val, func(r rune) bool { + switch r { + case '.', 'e', 'E': + return true + } + return false + }) + for _, part := range parts { + if !numUnderscoresOK(part) { + p.panicItemf(it, "Invalid float %q: underscores must be surrounded by digits", it.val) + } + } + if len(parts) > 0 && numHasLeadingZero(parts[0]) { + p.panicItemf(it, "Invalid float %q: cannot have leading zeroes", it.val) + } + if !numPeriodsOK(it.val) { + // As a special case, numbers like '123.' or '1.e2', + // which are valid as far as Go/strconv are concerned, + // must be rejected because TOML says that a fractional + // part consists of '.' followed by 1+ digits. + p.panicItemf(it, "Invalid float %q: '.' must be followed by one or more digits", it.val) + } + val := strings.Replace(it.val, "_", "", -1) + if val == "+nan" || val == "-nan" { // Go doesn't support this, but TOML spec does. + val = "nan" + } + num, err := strconv.ParseFloat(val, 64) + if err != nil { + if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { + p.panicErr(it, errParseRange{i: it.val, size: "float64"}) + } else { + p.panicItemf(it, "Invalid float value: %q", it.val) + } + } + return num, p.typeOfPrimitive(it) +} + +var dtTypes = []struct { + fmt string + zone *time.Location + next bool +}{ + {time.RFC3339Nano, time.Local, false}, + {"2006-01-02T15:04:05.999999999", internal.LocalDatetime, false}, + {"2006-01-02", internal.LocalDate, false}, + {"15:04:05.999999999", internal.LocalTime, false}, + + // tomlNext + {"2006-01-02T15:04Z07:00", time.Local, true}, + {"2006-01-02T15:04", internal.LocalDatetime, true}, + {"15:04", internal.LocalTime, true}, +} + +func (p *parser) valueDatetime(it item) (interface{}, tomlType) { + it.val = datetimeRepl.Replace(it.val) + var ( + t time.Time + ok bool + err error + ) + for _, dt := range dtTypes { + if dt.next && !p.tomlNext { + continue + } + t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone) + if err == nil { + ok = true + break + } + } + if !ok { + p.panicItemf(it, "Invalid TOML Datetime: %q.", it.val) + } + return t, p.typeOfPrimitive(it) +} + +func (p *parser) valueArray(it item) (interface{}, tomlType) { + p.setType(p.currentKey, tomlArray, it.pos) + + var ( + types []tomlType + + // Initialize to a non-nil empty slice. This makes it consistent with + // how S = [] decodes into a non-nil slice inside something like struct + // { S []string }. See #338 + array = []interface{}{} + ) + for it = p.next(); it.typ != itemArrayEnd; it = p.next() { + if it.typ == itemCommentStart { + p.expect(itemText) + continue + } + + val, typ := p.value(it, true) + array = append(array, val) + types = append(types, typ) + + // XXX: types isn't used here, we need it to record the accurate type + // information. + // + // Not entirely sure how to best store this; could use "key[0]", + // "key[1]" notation, or maybe store it on the Array type? + _ = types + } + return array, tomlArray +} + +func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tomlType) { + var ( + hash = make(map[string]interface{}) + outerContext = p.context + outerKey = p.currentKey + ) + + p.context = append(p.context, p.currentKey) + prevContext := p.context + p.currentKey = "" + + p.addImplicit(p.context) + p.addContext(p.context, parentIsArray) + + /// Loop over all table key/value pairs. + for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() { + if it.typ == itemCommentStart { + p.expect(itemText) + continue + } + + /// Read all key parts. + k := p.nextPos() + var key Key + for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() { + key = append(key, p.keyString(k)) + } + p.assertEqual(itemKeyEnd, k.typ) + + /// The current key is the last part. + p.currentKey = key[len(key)-1] + + /// All the other parts (if any) are the context; need to set each part + /// as implicit. + context := key[:len(key)-1] + for i := range context { + p.addImplicitContext(append(p.context, context[i:i+1]...)) + } + p.ordered = append(p.ordered, p.context.add(p.currentKey)) + + /// Set the value. + val, typ := p.value(p.next(), false) + p.set(p.currentKey, val, typ, it.pos) + hash[p.currentKey] = val + + /// Restore context. + p.context = prevContext + } + p.context = outerContext + p.currentKey = outerKey + return hash, tomlHash +} + +// numHasLeadingZero checks if this number has leading zeroes, allowing for '0', +// +/- signs, and base prefixes. +func numHasLeadingZero(s string) bool { + if len(s) > 1 && s[0] == '0' && !(s[1] == 'b' || s[1] == 'o' || s[1] == 'x') { // Allow 0b, 0o, 0x + return true + } + if len(s) > 2 && (s[0] == '-' || s[0] == '+') && s[1] == '0' { + return true + } + return false +} + // numUnderscoresOK checks whether each underscore in s is surrounded by // characters that are not underscores. func numUnderscoresOK(s string) bool { + switch s { + case "nan", "+nan", "-nan", "inf", "-inf", "+inf": + return true + } accept := false for _, r := range s { if r == '_' { if !accept { return false } - accept = false - continue } - accept = true + + // isHexadecimal is a superset of all the permissable characters + // surrounding an underscore. + accept = isHexadecimal(r) } return accept } @@ -338,13 +505,12 @@ func numPeriodsOK(s string) bool { return !period } -// establishContext sets the current context of the parser, -// where the context is either a hash or an array of hashes. Which one is -// set depends on the value of the `array` parameter. +// Set the current context of the parser, where the context is either a hash or +// an array of hashes, depending on the value of the `array` parameter. // // Establishing the context also makes sure that the key isn't a duplicate, and // will create implicit hashes automatically. -func (p *parser) establishContext(key Key, array bool) { +func (p *parser) addContext(key Key, array bool) { var ok bool // Always start at the top level and drill down for our context. @@ -383,7 +549,7 @@ func (p *parser) establishContext(key Key, array bool) { // list of tables for it. k := key[len(key)-1] if _, ok := hashContext[k]; !ok { - hashContext[k] = make([]map[string]interface{}, 0, 5) + hashContext[k] = make([]map[string]interface{}, 0, 4) } // Add a new table. But make sure the key hasn't already been used @@ -391,8 +557,7 @@ func (p *parser) establishContext(key Key, array bool) { if hash, ok := hashContext[k].([]map[string]interface{}); ok { hashContext[k] = append(hash, make(map[string]interface{})) } else { - p.panicf("Key '%s' was already created and cannot be used as "+ - "an array.", keyContext) + p.panicf("Key '%s' was already created and cannot be used as an array.", key) } } else { p.setValue(key[len(key)-1], make(map[string]interface{})) @@ -400,15 +565,22 @@ func (p *parser) establishContext(key Key, array bool) { p.context = append(p.context, key[len(key)-1]) } +// set calls setValue and setType. +func (p *parser) set(key string, val interface{}, typ tomlType, pos Position) { + p.setValue(key, val) + p.setType(key, typ, pos) +} + // setValue sets the given key to the given value in the current context. // It will make sure that the key hasn't already been defined, account for // implicit key groups. func (p *parser) setValue(key string, value interface{}) { - var tmpHash interface{} - var ok bool - - hash := p.mapping - keyContext := make(Key, 0) + var ( + tmpHash interface{} + ok bool + hash = p.mapping + keyContext Key + ) for _, k := range p.context { keyContext = append(keyContext, k) if tmpHash, ok = hash[k]; !ok { @@ -422,24 +594,26 @@ func (p *parser) setValue(key string, value interface{}) { case map[string]interface{}: hash = t default: - p.bug("Expected hash to have type 'map[string]interface{}', but "+ - "it has '%T' instead.", tmpHash) + p.panicf("Key '%s' has already been defined.", keyContext) } } keyContext = append(keyContext, key) if _, ok := hash[key]; ok { - // Typically, if the given key has already been set, then we have - // to raise an error since duplicate keys are disallowed. However, - // it's possible that a key was previously defined implicitly. In this - // case, it is allowed to be redefined concretely. (See the - // `tests/valid/implicit-and-explicit-after.toml` test in `toml-test`.) + // Normally redefining keys isn't allowed, but the key could have been + // defined implicitly and it's allowed to be redefined concretely. (See + // the `valid/implicit-and-explicit-after.toml` in toml-test) // // But we have to make sure to stop marking it as an implicit. (So that // another redefinition provokes an error.) // // Note that since it has already been defined (as a hash), we don't // want to overwrite it. So our business is done. + if p.isArray(keyContext) { + p.removeImplicit(keyContext) + hash[key] = value + return + } if p.isImplicit(keyContext) { p.removeImplicit(keyContext) return @@ -449,41 +623,37 @@ func (p *parser) setValue(key string, value interface{}) { // key, which is *always* wrong. p.panicf("Key '%s' has already been defined.", keyContext) } + hash[key] = value } -// setType sets the type of a particular value at a given key. -// It should be called immediately AFTER setValue. +// setType sets the type of a particular value at a given key. It should be +// called immediately AFTER setValue. // // Note that if `key` is empty, then the type given will be applied to the // current context (which is either a table or an array of tables). -func (p *parser) setType(key string, typ tomlType) { +func (p *parser) setType(key string, typ tomlType, pos Position) { keyContext := make(Key, 0, len(p.context)+1) - for _, k := range p.context { - keyContext = append(keyContext, k) - } + keyContext = append(keyContext, p.context...) if len(key) > 0 { // allow type setting for hashes keyContext = append(keyContext, key) } - p.types[keyContext.String()] = typ + // Special case to make empty keys ("" = 1) work. + // Without it it will set "" rather than `""`. + // TODO: why is this needed? And why is this only needed here? + if len(keyContext) == 0 { + keyContext = Key{""} + } + p.keyInfo[keyContext.String()] = keyInfo{tomlType: typ, pos: pos} } -// addImplicit sets the given Key as having been created implicitly. -func (p *parser) addImplicit(key Key) { - p.implicits[key.String()] = true -} - -// removeImplicit stops tagging the given key as having been implicitly -// created. -func (p *parser) removeImplicit(key Key) { - p.implicits[key.String()] = false -} - -// isImplicit returns true if the key group pointed to by the key was created -// implicitly. -func (p *parser) isImplicit(key Key) bool { - return p.implicits[key.String()] -} +// Implicit keys need to be created when tables are implied in "a.b.c.d = 1" and +// "[a.b.c]" (the "a", "b", and "c" hashes are never created explicitly). +func (p *parser) addImplicit(key Key) { p.implicits[key.String()] = struct{}{} } +func (p *parser) removeImplicit(key Key) { delete(p.implicits, key.String()) } +func (p *parser) isImplicit(key Key) bool { _, ok := p.implicits[key.String()]; return ok } +func (p *parser) isArray(key Key) bool { return p.keyInfo[key.String()].tomlType == tomlArray } +func (p *parser) addImplicitContext(key Key) { p.addImplicit(key); p.addContext(key, false) } // current returns the full key name of the current context. func (p *parser) current() string { @@ -497,24 +667,67 @@ func (p *parser) current() string { } func stripFirstNewline(s string) string { - if len(s) == 0 || s[0] != '\n' { - return s + if len(s) > 0 && s[0] == '\n' { + return s[1:] } - return s[1:] + if len(s) > 1 && s[0] == '\r' && s[1] == '\n' { + return s[2:] + } + return s } -func stripEscapedWhitespace(s string) string { - esc := strings.Split(s, "\\\n") - if len(esc) > 1 { - for i := 1; i < len(esc); i++ { - esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace) +// stripEscapedNewlines removes whitespace after line-ending backslashes in +// multiline strings. +// +// A line-ending backslash is an unescaped \ followed only by whitespace until +// the next newline. After a line-ending backslash, all whitespace is removed +// until the next non-whitespace character. +func (p *parser) stripEscapedNewlines(s string) string { + var b strings.Builder + var i int + for { + ix := strings.Index(s[i:], `\`) + if ix < 0 { + b.WriteString(s) + return b.String() } + i += ix + + if len(s) > i+1 && s[i+1] == '\\' { + // Escaped backslash. + i += 2 + continue + } + // Scan until the next non-whitespace. + j := i + 1 + whitespaceLoop: + for ; j < len(s); j++ { + switch s[j] { + case ' ', '\t', '\r', '\n': + default: + break whitespaceLoop + } + } + if j == i+1 { + // Not a whitespace escape. + i++ + continue + } + if !strings.Contains(s[i:j], "\n") { + // This is not a line-ending backslash. + // (It's a bad escape sequence, but we can let + // replaceEscapes catch it.) + i++ + continue + } + b.WriteString(s[:i]) + s = s[j:] + i = 0 } - return strings.Join(esc, "") } -func (p *parser) replaceEscapes(str string) string { - var replaced []rune +func (p *parser) replaceEscapes(it item, str string) string { + replaced := make([]rune, 0, len(str)) s := []byte(str) r := 0 for r < len(s) { @@ -532,7 +745,8 @@ func (p *parser) replaceEscapes(str string) string { switch s[r] { default: p.bug("Expected valid escape code after \\, but got %q.", s[r]) - return "" + case ' ', '\t': + p.panicItemf(it, "invalid escape: '\\%c'", s[r]) case 'b': replaced = append(replaced, rune(0x0008)) r += 1 @@ -548,24 +762,35 @@ func (p *parser) replaceEscapes(str string) string { case 'r': replaced = append(replaced, rune(0x000D)) r += 1 + case 'e': + if p.tomlNext { + replaced = append(replaced, rune(0x001B)) + r += 1 + } case '"': replaced = append(replaced, rune(0x0022)) r += 1 case '\\': replaced = append(replaced, rune(0x005C)) r += 1 + case 'x': + if p.tomlNext { + escaped := p.asciiEscapeToUnicode(it, s[r+1:r+3]) + replaced = append(replaced, escaped) + r += 3 + } case 'u': // At this point, we know we have a Unicode escape of the form // `uXXXX` at [r, r+5). (Because the lexer guarantees this // for us.) - escaped := p.asciiEscapeToUnicode(s[r+1 : r+5]) + escaped := p.asciiEscapeToUnicode(it, s[r+1:r+5]) replaced = append(replaced, escaped) r += 5 case 'U': // At this point, we know we have a Unicode escape of the form // `uXXXX` at [r, r+9). (Because the lexer guarantees this // for us.) - escaped := p.asciiEscapeToUnicode(s[r+1 : r+9]) + escaped := p.asciiEscapeToUnicode(it, s[r+1:r+9]) replaced = append(replaced, escaped) r += 9 } @@ -573,20 +798,14 @@ func (p *parser) replaceEscapes(str string) string { return string(replaced) } -func (p *parser) asciiEscapeToUnicode(bs []byte) rune { +func (p *parser) asciiEscapeToUnicode(it item, bs []byte) rune { s := string(bs) hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32) if err != nil { - p.bug("Could not parse '%s' as a hexadecimal number, but the "+ - "lexer claims it's OK: %s", s, err) + p.bug("Could not parse '%s' as a hexadecimal number, but the lexer claims it's OK: %s", s, err) } if !utf8.ValidRune(rune(hex)) { - p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s) + p.panicItemf(it, "Escaped character '\\u%s' is not valid UTF-8.", s) } return rune(hex) } - -func isStringType(ty itemType) bool { - return ty == itemString || ty == itemMultilineString || - ty == itemRawString || ty == itemRawMultilineString -} diff --git a/vendor/github.com/BurntSushi/toml/session.vim b/vendor/github.com/BurntSushi/toml/session.vim deleted file mode 100644 index 562164be..00000000 --- a/vendor/github.com/BurntSushi/toml/session.vim +++ /dev/null @@ -1 +0,0 @@ -au BufWritePost *.go silent!make tags > /dev/null 2>&1 diff --git a/vendor/github.com/BurntSushi/toml/type_fields.go b/vendor/github.com/BurntSushi/toml/type_fields.go index 608997c2..254ca82e 100644 --- a/vendor/github.com/BurntSushi/toml/type_fields.go +++ b/vendor/github.com/BurntSushi/toml/type_fields.go @@ -70,8 +70,8 @@ func typeFields(t reflect.Type) []field { next := []field{{typ: t}} // Count of queued names for current level and the next. - count := map[reflect.Type]int{} - nextCount := map[reflect.Type]int{} + var count map[reflect.Type]int + var nextCount map[reflect.Type]int // Types already visited at an earlier level. visited := map[reflect.Type]bool{} diff --git a/vendor/github.com/BurntSushi/toml/type_check.go b/vendor/github.com/BurntSushi/toml/type_toml.go similarity index 75% rename from vendor/github.com/BurntSushi/toml/type_check.go rename to vendor/github.com/BurntSushi/toml/type_toml.go index c73f8afc..4e90d773 100644 --- a/vendor/github.com/BurntSushi/toml/type_check.go +++ b/vendor/github.com/BurntSushi/toml/type_toml.go @@ -16,7 +16,7 @@ func typeEqual(t1, t2 tomlType) bool { return t1.typeString() == t2.typeString() } -func typeIsHash(t tomlType) bool { +func typeIsTable(t tomlType) bool { return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash) } @@ -68,24 +68,3 @@ func (p *parser) typeOfPrimitive(lexItem item) tomlType { p.bug("Cannot infer primitive type of lex item '%s'.", lexItem) panic("unreachable") } - -// typeOfArray returns a tomlType for an array given a list of types of its -// values. -// -// In the current spec, if an array is homogeneous, then its type is always -// "Array". If the array is not homogeneous, an error is generated. -func (p *parser) typeOfArray(types []tomlType) tomlType { - // Empty arrays are cool. - if len(types) == 0 { - return tomlArray - } - - theType := types[0] - for _, t := range types[1:] { - if !typeEqual(theType, t) { - p.panicf("Array contains values of type '%s' and '%s', but "+ - "arrays must be homogeneous.", theType, t) - } - } - return tomlArray -} diff --git a/vendor/github.com/andygrunwald/go-jira/.gitignore b/vendor/github.com/andygrunwald/go-jira/.gitignore index 1e57f8a7..e2006062 100644 --- a/vendor/github.com/andygrunwald/go-jira/.gitignore +++ b/vendor/github.com/andygrunwald/go-jira/.gitignore @@ -27,3 +27,4 @@ _testmain.go *.prof *.iml .idea +.DS_Store \ No newline at end of file diff --git a/vendor/github.com/andygrunwald/go-jira/.travis.yml b/vendor/github.com/andygrunwald/go-jira/.travis.yml deleted file mode 100644 index a0b62b63..00000000 --- a/vendor/github.com/andygrunwald/go-jira/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: go - -sudo: false - -go: - - "1.9.x" - - "1.10.x" - - "1.11.x" - - "1.12.x" - - "1.13.x" - -before_install: - - go get -t ./... - -matrix: - allow_failures: - - go: 1.13.x - -script: - - GOMAXPROCS=4 GORACE="halt_on_error=1" go test -race -v ./... diff --git a/vendor/github.com/andygrunwald/go-jira/CHANGELOG.md b/vendor/github.com/andygrunwald/go-jira/CHANGELOG.md index e893cd7a..5915ef43 100644 --- a/vendor/github.com/andygrunwald/go-jira/CHANGELOG.md +++ b/vendor/github.com/andygrunwald/go-jira/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.13.0](https://github.com/andygrunwald/go-jira/compare/v1.11.1...v1.13.0) (2020-10-25) + + +### Features + +* add AddRemoteLink method ([f200e15](https://github.com/andygrunwald/go-jira/commit/f200e158b997a303db081cbbc5a9d8ad5d89566d)), closes [/developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2](https://github.com/andygrunwald//developer.atlassian.com/cloud/jira/platform/rest/v2//issues/api-rest-api-2) +* Add Names support on Issue struct ([#278](https://github.com/andygrunwald/go-jira/issues/278)) ([1fc10e0](https://github.com/andygrunwald/go-jira/commit/1fc10e0606784f745673ccc4d8d706c36f385a7a)) +* Extend Makefile for more source code quality targets ([5e52236](https://github.com/andygrunwald/go-jira/commit/5e5223631a29d10a13e598318a6abe47384e2982)) +* **context:** Add support for context package ([e1f4265](https://github.com/andygrunwald/go-jira/commit/e1f4265e2b467b938fe0c095caf6d36f3136d2ff)) +* **issues:** Add GetEditMeta on issue ([a783764](https://github.com/andygrunwald/go-jira/commit/a783764b52dc890773658ddd0483a9d0393e385d)), closes [/docs.atlassian.com/DAC/rest/jira/6.1.html#d2e1364](https://github.com/andygrunwald//docs.atlassian.com/DAC/rest/jira/6.1.html/issues/d2e1364) +* **IssueService:** allow empty JQL ([#268](https://github.com/andygrunwald/go-jira/issues/268)) ([4b91cf2](https://github.com/andygrunwald/go-jira/commit/4b91cf2b135355de7ecee41727c3e65f4e7067bc)) +* **project:** Add cronjob to check for stale issues ([#287](https://github.com/andygrunwald/go-jira/issues/287)) ([2096b04](https://github.com/andygrunwald/go-jira/commit/2096b04e52b434c1fb1c841bab487a94674a271e)) +* **project:** Add GitHub Actions testing workflow ([#289](https://github.com/andygrunwald/go-jira/issues/289)) ([80c0282](https://github.com/andygrunwald/go-jira/commit/80c02828ca9e4eb0e4a1877275baae14d330a2d9)), closes [#290](https://github.com/andygrunwald/go-jira/issues/290) +* **project:** Add workflow to greet new contributors ([#288](https://github.com/andygrunwald/go-jira/issues/288)) ([c357b61](https://github.com/andygrunwald/go-jira/commit/c357b61a40f62a919ebd94a555390958f99c8db7)) + + +### Bug Fixes + +* change millisecond time format ([8c77107](https://github.com/andygrunwald/go-jira/commit/8c77107df3757c4ec5eae6e9d7c018618e708bfa)) +* paging with load balancer going to endless loop ([19d3fc0](https://github.com/andygrunwald/go-jira/commit/19d3fc0aecde547ffe1ab547c5ffb6c7972d387c)), closes [#260](https://github.com/andygrunwald/go-jira/issues/260) +* **issue:** IssueService.Search() with a not empty JQL triggers 400 bad request ([#292](https://github.com/andygrunwald/go-jira/issues/292)) ([8b64c7f](https://github.com/andygrunwald/go-jira/commit/8b64c7f005fbceb11fa43a7aff3de61eb3166fca)), closes [#291](https://github.com/andygrunwald/go-jira/issues/291) +* **IssueService.GetWatchers:** UserService.GetByAccountID support accountId params ([436469b](https://github.com/andygrunwald/go-jira/commit/436469b62d4d62037f380b38c918a13f4a5f0ab2)) +* **product:** Make product naming consistent, rename JIRA to Jira ([#286](https://github.com/andygrunwald/go-jira/issues/286)) ([146229d](https://github.com/andygrunwald/go-jira/commit/146229d2ab58a3fb128ddc8dcbe03aff72e20857)), closes [#284](https://github.com/andygrunwald/go-jira/issues/284) +* **tests:** Fix TestIssueService_PostAttachment unit test ([f6b1dca](https://github.com/andygrunwald/go-jira/commit/f6b1dcafcfdd8fe69f842b1053c4030da6c97c7f)) +* removing the use of username field in searching for users ([#297](https://github.com/andygrunwald/go-jira/issues/297)) ([f50cb07](https://github.com/andygrunwald/go-jira/commit/f50cb07b297d79138b13e5ab49ea33965d32f5c1)) + ## [1.12.0](https://github.com/andygrunwald/go-jira/compare/v1.11.1...v1.12.0) (2019-12-14) @@ -76,6 +102,3 @@ All notable changes to this project will be documented in this file. See [standa * Add ResolutionService to retrieve resolutions ([fb1ce22](https://github.com/andygrunwald/go-jira/commit/fb1ce22)) * Add status category constants ([6223ddd](https://github.com/andygrunwald/go-jira/commit/6223ddd)) * Add StatusCategory GetList ([049a756](https://github.com/andygrunwald/go-jira/commit/049a756)) - - - diff --git a/vendor/github.com/andygrunwald/go-jira/Makefile b/vendor/github.com/andygrunwald/go-jira/Makefile index d9ddca95..e9b063d7 100644 --- a/vendor/github.com/andygrunwald/go-jira/Makefile +++ b/vendor/github.com/andygrunwald/go-jira/Makefile @@ -1,2 +1,25 @@ -test: - go test -v ./... \ No newline at end of file +.DEFAULT_GOAL := help + +.PHONY: help +help: ## Outputs the help. + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: test +test: ## Runs all unit, integration and example tests. + go test -race -v ./... + +.PHONY: vet +vet: ## Runs go vet (to detect suspicious constructs). + go vet ./... + +.PHONY: fmt +fmt: ## Runs go fmt (to check for go coding guidelines). + gofmt -d -s . + +.PHONY: staticcheck +staticcheck: ## Runs static analysis to prevend bugs, foster code simplicity, performance and editor integration. + go install honnef.co/go/tools/cmd/staticcheck@2022.1 + staticcheck ./... + +.PHONY: all +all: test vet fmt staticcheck ## Runs all source code quality targets (like test, vet, fmt, staticcheck) diff --git a/vendor/github.com/andygrunwald/go-jira/PULL_REQUEST_TEMPLATE.md b/vendor/github.com/andygrunwald/go-jira/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 26ee7854..00000000 --- a/vendor/github.com/andygrunwald/go-jira/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,15 +0,0 @@ -# PR Description - -_What does this fix or add?_ - -# Checklist - -* [ ] Tests added - * [ ] Good Path - * [ ] Error Path -* [ ] Commits follow conventions described here: - * [ ] [https://conventionalcommits.org/en/v1.0.0-beta.4/#summary](https://conventionalcommits.org/en/v1.0.0-beta.4/#summary) - * [ ] [https://chris.beams.io/posts/git-commit/#seven-rules](https://chris.beams.io/posts/git-commit/#seven-rules) -* [ ] Commits are squashed such that - * [ ] There is 1 commit per isolated change -* [ ] I've not made extraneous commits/changes that are unrelated to my change. diff --git a/vendor/github.com/andygrunwald/go-jira/README.md b/vendor/github.com/andygrunwald/go-jira/README.md index 64f2263b..571d4c30 100644 --- a/vendor/github.com/andygrunwald/go-jira/README.md +++ b/vendor/github.com/andygrunwald/go-jira/README.md @@ -1,26 +1,29 @@ # go-jira [![GoDoc](https://godoc.org/github.com/andygrunwald/go-jira?status.svg)](https://godoc.org/github.com/andygrunwald/go-jira) -[![Build Status](https://travis-ci.org/andygrunwald/go-jira.svg?branch=master)](https://travis-ci.org/andygrunwald/go-jira) +[![Build Status](https://github.com/andygrunwald/go-jira/actions/workflows/testing.yml/badge.svg)](https://github.com/andygrunwald/go-jira/actions/workflows/testing.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/andygrunwald/go-jira)](https://goreportcard.com/report/github.com/andygrunwald/go-jira) -[Go](https://golang.org/) client library for [Atlassian JIRA](https://www.atlassian.com/software/jira). +[Go](https://golang.org/) client library for [Atlassian Jira](https://www.atlassian.com/software/jira). -![Go client library for Atlassian JIRA](./img/logo_small.png "Go client library for Atlassian JIRA.") +![Go client library for Atlassian Jira](./img/logo_small.png "Go client library for Atlassian Jira.") ## Features * Authentication (HTTP Basic, OAuth, Session Cookie) * Create and retrieve issues * Create and retrieve issue transitions (status updates) -* Call every API endpoint of the JIRA, even if it is not directly implemented in this library +* Call every API endpoint of the Jira, even if it is not directly implemented in this library -This package is not JIRA API complete (yet), but you can call every API endpoint you want. See [Call a not implemented API endpoint](#call-a-not-implemented-api-endpoint) how to do this. For all possible API endpoints of JIRA have a look at [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/). +This package is not Jira API complete (yet), but you can call every API endpoint you want. See [Call a not implemented API endpoint](#call-a-not-implemented-api-endpoint) how to do this. For all possible API endpoints of Jira have a look at [latest Jira REST API documentation](https://docs.atlassian.com/jira/REST/latest/). ## Requirements -* Go >= 1.8 -* JIRA v6.3.4 & v7.1.2. +* Go >= 1.14 +* Jira v6.3.4 & v7.1.2. + +Note that we also run our tests against 1.13, though only the last two versions +of Go are officially supported. ## Installation @@ -52,7 +55,7 @@ go test -v ./... Please have a look at the [GoDoc documentation](https://godoc.org/github.com/andygrunwald/go-jira) for a detailed API description. -The [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/) was the base document for this package. +The [latest Jira REST API documentation](https://docs.atlassian.com/jira/REST/latest/) was the base document for this package. ## Examples @@ -68,7 +71,7 @@ package main import ( "fmt" - "github.com/andygrunwald/go-jira" + jira "github.com/andygrunwald/go-jira" ) func main() { @@ -92,9 +95,11 @@ an `http.Client`. That client can then be passed into the `NewClient` function For convenience, capability for basic and cookie-based authentication is included in the main library. -#### Basic auth example +#### Token (Jira on Atlassian Cloud) -A more thorough, [runnable example](examples/basicauth/main.go) is provided in the examples directory. **It's worth noting that using passwords in basic auth is now deprecated and will be removed.** Jira gives you the ability to [create tokens now.](https://confluence.atlassian.com/cloud/api-tokens-938839638.html) +Token-based authentication uses the basic authentication scheme, with a user-generated API token in place of a user's password. You can generate a token for your user [here](https://id.atlassian.com/manage-profile/security/api-tokens). Additional information about Atlassian Cloud API tokens can be found [here](https://confluence.atlassian.com/cloud/api-tokens-938839638.html). + +A more thorough, [runnable example](examples/basicauth/main.go) is provided in the examples directory. ```go func main() { @@ -111,14 +116,15 @@ func main() { } ``` -#### Authenticate with session cookie [DEPRECATED] +#### Basic (self-hosted Jira) -JIRA [deprecated this authentication method.](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/) It's not longer available for use. +Password-based API authentication works for self-hosted Jira **only**, and has been [deprecated for users of Atlassian Cloud](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/). +The above token authentication example may be used, substituting a user's password for a generated token. #### Authenticate with OAuth -If you want to connect via OAuth to your JIRA Cloud instance checkout the [example of using OAuth authentication with JIRA in Go](https://gist.github.com/Lupus/edafe9a7c5c6b13407293d795442fe67) by [@Lupus](https://github.com/Lupus). +If you want to connect via OAuth to your Jira Cloud instance checkout the [example of using OAuth authentication with Jira in Go](https://gist.github.com/Lupus/edafe9a7c5c6b13407293d795442fe67) by [@Lupus](https://github.com/Lupus). For more details have a look at the [issue #56](https://github.com/andygrunwald/go-jira/issues/56). @@ -173,11 +179,62 @@ func main() { } ``` +### Change an issue status + +This is how one can change an issue status. In this example, we change the issue from "To Do" to "In Progress." + +```go +package main + +import ( + "fmt" + "github.com/andygrunwald/go-jira" +) + +func main() { + base := "https://my.jira.com" + tp := jira.BasicAuthTransport{ + Username: "username", + Password: "token", + } + + jiraClient, err := jira.NewClient(tp.Client(), base) + if err != nil { + panic(err) + } + + issue, _, _ := jiraClient.Issue.Get("FART-1", nil) + currentStatus := issue.Fields.Status.Name + fmt.Printf("Current status: %s\n", currentStatus) + + var transitionID string + possibleTransitions, _, _ := jiraClient.Issue.GetTransitions("FART-1") + for _, v := range possibleTransitions { + if v.Name == "In Progress" { + transitionID = v.ID + break + } + } + + jiraClient.Issue.DoTransition("FART-1", transitionID) + issue, _, _ = jiraClient.Issue.Get(testIssueID, nil) + fmt.Printf("Status after transition: %+v\n", issue.Fields.Status.Name) +} +``` +### Get all the issues for JQL with Pagination +Jira API has limit on maxResults it can return. You may have a usecase where you need to get all issues for given JQL. +This example shows reference implementation of GetAllIssues function which does pagination on Jira API to get all the issues for given JQL + +please look at [Pagination Example](https://github.com/andygrunwald/go-jira/blob/master/examples/pagination/main.go) + + + + ### Call a not implemented API endpoint -Not all API endpoints of the JIRA API are implemented into *go-jira*. +Not all API endpoints of the Jira API are implemented into *go-jira*. But you can call them anyway: -Lets get all public projects of [Atlassian`s JIRA instance](https://jira.atlassian.com/). +Lets get all public projects of [Atlassian`s Jira instance](https://jira.atlassian.com/). ```go package main @@ -209,7 +266,7 @@ func main() { // ... // BAM: Bamboo - // BAMJ: Bamboo JIRA Plugin + // BAMJ: Bamboo Jira Plugin // CLOV: Clover // CONF: Confluence // ... @@ -218,7 +275,7 @@ func main() { ## Implementations -* [andygrunwald/jitic](https://github.com/andygrunwald/jitic) - The JIRA Ticket Checker +* [andygrunwald/jitic](https://github.com/andygrunwald/jitic) - The Jira Ticket Checker ## Code structure @@ -226,7 +283,7 @@ The code structure of this package was inspired by [google/go-github](https://gi There is one main part (the client). Based on this main client the other endpoints, like Issues or Authentication are extracted in services. E.g. `IssueService` or `AuthenticationService`. -These services own a responsibility of the single endpoints / usecases of JIRA. +These services own a responsibility of the single endpoints / usecases of Jira. ## Contribution @@ -258,7 +315,7 @@ You can read more about them at https://developer.atlassian.com/blog/2016/04/clo ## Releasing -Install `standard-version` +Install [standard-version](https://github.com/conventional-changelog/standard-version) ```bash npm i -g standard-version ``` diff --git a/vendor/github.com/andygrunwald/go-jira/authentication.go b/vendor/github.com/andygrunwald/go-jira/authentication.go index f848a1d5..fcaf8e10 100644 --- a/vendor/github.com/andygrunwald/go-jira/authentication.go +++ b/vendor/github.com/andygrunwald/go-jira/authentication.go @@ -1,6 +1,7 @@ package jira import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -14,9 +15,9 @@ const ( authTypeSession = 2 ) -// AuthenticationService handles authentication for the JIRA instance / API. +// AuthenticationService handles authentication for the Jira instance / API. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#authentication +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#authentication type AuthenticationService struct { client *Client @@ -30,7 +31,7 @@ type AuthenticationService struct { password string } -// Session represents a Session JSON response by the JIRA API. +// Session represents a Session JSON response by the Jira API. type Session struct { Self string `json:"self,omitempty"` Name string `json:"name,omitempty"` @@ -47,16 +48,16 @@ type Session struct { Cookies []*http.Cookie } -// AcquireSessionCookie creates a new session for a user in JIRA. -// Once a session has been successfully created it can be used to access any of JIRA's remote APIs and also the web UI by passing the appropriate HTTP Cookie header. +// AcquireSessionCookieWithContext creates a new session for a user in Jira. +// Once a session has been successfully created it can be used to access any of Jira's remote APIs and also the web UI by passing the appropriate HTTP Cookie header. // The header will by automatically applied to every API request. // Note that it is generally preferrable to use HTTP BASIC authentication with the REST API. -// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user). +// However, this resource may be used to mimic the behaviour of Jira's log-in page (e.g. to display log-in errors to a user). // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session // // Deprecated: Use CookieAuthTransport instead -func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) { +func (s *AuthenticationService) AcquireSessionCookieWithContext(ctx context.Context, username, password string) (bool, error) { apiEndpoint := "rest/auth/1/session" body := struct { Username string `json:"username"` @@ -66,7 +67,7 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string) password, } - req, err := s.client.NewRequest("POST", apiEndpoint, body) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, body) if err != nil { return false, err } @@ -79,10 +80,10 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string) } if err != nil { - return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err) + return false, fmt.Errorf("auth at Jira instance failed (HTTP(S) request). %s", err) } if resp != nil && resp.StatusCode != 200 { - return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). Status code: %d", resp.StatusCode) + return false, fmt.Errorf("auth at Jira instance failed (HTTP(S) request). Status code: %d", resp.StatusCode) } s.client.session = session @@ -91,7 +92,14 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string) return true, nil } -// SetBasicAuth sets username and password for the basic auth against the JIRA instance. +// AcquireSessionCookie wraps AcquireSessionCookieWithContext using the background context. +// +// Deprecated: Use CookieAuthTransport instead +func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) { + return s.AcquireSessionCookieWithContext(context.Background(), username, password) +} + +// SetBasicAuth sets username and password for the basic auth against the Jira instance. // // Deprecated: Use BasicAuthTransport instead func (s *AuthenticationService) SetBasicAuth(username, password string) { @@ -100,7 +108,7 @@ func (s *AuthenticationService) SetBasicAuth(username, password string) { s.authType = authTypeBasic } -// Authenticated reports if the current Client has authentication details for JIRA +// Authenticated reports if the current Client has authentication details for Jira func (s *AuthenticationService) Authenticated() bool { if s != nil { if s.authType == authTypeSession { @@ -113,29 +121,30 @@ func (s *AuthenticationService) Authenticated() bool { return false } -// Logout logs out the current user that has been authenticated and the session in the client is destroyed. +// LogoutWithContext logs out the current user that has been authenticated and the session in the client is destroyed. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session // // Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the // client anymore -func (s *AuthenticationService) Logout() error { +func (s *AuthenticationService) LogoutWithContext(ctx context.Context) error { if s.authType != authTypeSession || s.client.session == nil { return fmt.Errorf("no user is authenticated") } apiEndpoint := "rest/auth/1/session" - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { - return fmt.Errorf("Creating the request to log the user out failed : %s", err) + return fmt.Errorf("creating the request to log the user out failed : %s", err) } resp, err := s.client.Do(req, nil) if err != nil { - return fmt.Errorf("Error sending the logout request: %s", err) + return fmt.Errorf("error sending the logout request: %s", err) } + defer resp.Body.Close() if resp.StatusCode != 204 { - return fmt.Errorf("The logout was unsuccessful with status %d", resp.StatusCode) + return fmt.Errorf("the logout was unsuccessful with status %d", resp.StatusCode) } // If logout successful, delete session @@ -145,43 +154,55 @@ func (s *AuthenticationService) Logout() error { } -// GetCurrentUser gets the details of the current user. +// Logout wraps LogoutWithContext using the background context. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session -func (s *AuthenticationService) GetCurrentUser() (*Session, error) { +// Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the +// client anymore +func (s *AuthenticationService) Logout() error { + return s.LogoutWithContext(context.Background()) +} + +// GetCurrentUserWithContext gets the details of the current user. +// +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session +func (s *AuthenticationService) GetCurrentUserWithContext(ctx context.Context) (*Session, error) { if s == nil { - return nil, fmt.Errorf("AUthenticaiton Service is not instantiated") + return nil, fmt.Errorf("authentication Service is not instantiated") } if s.authType != authTypeSession || s.client.session == nil { - return nil, fmt.Errorf("No user is authenticated yet") + return nil, fmt.Errorf("no user is authenticated yet") } apiEndpoint := "rest/auth/1/session" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { - return nil, fmt.Errorf("Could not create request for getting user info : %s", err) + return nil, fmt.Errorf("could not create request for getting user info : %s", err) } resp, err := s.client.Do(req, nil) if err != nil { - return nil, fmt.Errorf("Error sending request to get user info : %s", err) + return nil, fmt.Errorf("error sending request to get user info : %s", err) } - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Getting user info failed with status : %d", resp.StatusCode) - } - defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, fmt.Errorf("getting user info failed with status : %d", resp.StatusCode) + } ret := new(Session) data, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("Couldn't read body from the response : %s", err) + return nil, fmt.Errorf("couldn't read body from the response : %s", err) } err = json.Unmarshal(data, &ret) if err != nil { - return nil, fmt.Errorf("Could not unmarshall received user info : %s", err) + return nil, fmt.Errorf("could not unmarshall received user info : %s", err) } return ret, nil } + +// GetCurrentUser wraps GetCurrentUserWithContext using the background context. +func (s *AuthenticationService) GetCurrentUser() (*Session, error) { + return s.GetCurrentUserWithContext(context.Background()) +} diff --git a/vendor/github.com/andygrunwald/go-jira/board.go b/vendor/github.com/andygrunwald/go-jira/board.go index 3daa4c84..890d6e0a 100644 --- a/vendor/github.com/andygrunwald/go-jira/board.go +++ b/vendor/github.com/andygrunwald/go-jira/board.go @@ -1,14 +1,15 @@ package jira import ( + "context" "fmt" "strconv" "time" ) -// BoardService handles Agile Boards for the JIRA instance / API. +// BoardService handles Agile Boards for the Jira instance / API. // -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/server/ +// Jira API docs: https://docs.atlassian.com/jira-software/REST/server/ type BoardService struct { client *Client } @@ -22,7 +23,7 @@ type BoardsList struct { Values []Board `json:"values" structs:"values"` } -// Board represents a JIRA agile board +// Board represents a Jira agile board type Board struct { ID int `json:"id,omitempty" structs:"id,omitempty"` Self string `json:"self,omitempty" structs:"self,omitempty"` @@ -62,7 +63,7 @@ type SprintsList struct { Values []Sprint `json:"values" structs:"values"` } -// Sprint represents a sprint on JIRA agile board +// Sprint represents a sprint on Jira agile board type Sprint struct { ID int `json:"id" structs:"id"` Name string `json:"name" structs:"name"` @@ -116,6 +117,8 @@ type BoardConfigurationColumnConfig struct { type BoardConfigurationColumn struct { Name string `json:"name"` Status []BoardConfigurationColumnStatus `json:"statuses"` + Min int `json:"min,omitempty"` + Max int `json:"max,omitempty"` } // BoardConfigurationColumnStatus represents a status in the column configuration @@ -124,16 +127,16 @@ type BoardConfigurationColumnStatus struct { Self string `json:"self"` } -// GetAllBoards will returns all boards. This only includes boards that the user has permission to view. +// GetAllBoardsWithContext will returns all boards. This only includes boards that the user has permission to view. // -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getAllBoards -func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getAllBoards +func (s *BoardService) GetAllBoardsWithContext(ctx context.Context, opt *BoardListOptions) (*BoardsList, *Response, error) { apiEndpoint := "rest/agile/1.0/board" url, err := addOptions(apiEndpoint, opt) if err != nil { return nil, nil, err } - req, err := s.client.NewRequest("GET", url, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, nil, err } @@ -148,13 +151,18 @@ func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Respon return boards, resp, err } -// GetBoard will returns the board for the given boardID. +// GetAllBoards wraps GetAllBoardsWithContext using the background context. +func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Response, error) { + return s.GetAllBoardsWithContext(context.Background(), opt) +} + +// GetBoardWithContext will returns the board for the given boardID. // This board will only be returned if the user has permission to view it. // -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getBoard -func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getBoard +func (s *BoardService) GetBoardWithContext(ctx context.Context, boardID int) (*Board, *Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -169,17 +177,22 @@ func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) { return board, resp, nil } -// CreateBoard creates a new board. Board name, type and filter Id is required. +// GetBoard wraps GetBoardWithContext using the background context. +func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) { + return s.GetBoardWithContext(context.Background(), boardID) +} + +// CreateBoardWithContext creates a new board. Board name, type and filter Id is required. // name - Must be less than 255 characters. // type - Valid values: scrum, kanban // filterId - Id of a filter that the user has permissions to view. // Note, if the user does not have the 'Create shared objects' permission and tries to create a shared board, a private // board will be created instead (remember that board sharing depends on the filter sharing). // -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-createBoard -func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-createBoard +func (s *BoardService) CreateBoardWithContext(ctx context.Context, board *Board) (*Board, *Response, error) { apiEndpoint := "rest/agile/1.0/board" - req, err := s.client.NewRequest("POST", apiEndpoint, board) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, board) if err != nil { return nil, nil, err } @@ -194,12 +207,18 @@ func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) { return responseBoard, resp, nil } -// DeleteBoard will delete an agile board. +// CreateBoard wraps CreateBoardWithContext using the background context. +func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) { + return s.CreateBoardWithContext(context.Background(), board) +} + +// DeleteBoardWithContext will delete an agile board. // -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-deleteBoard -func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-deleteBoard +// Caller must close resp.Body +func (s *BoardService) DeleteBoardWithContext(ctx context.Context, boardID int) (*Board, *Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -211,11 +230,17 @@ func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) { return nil, resp, err } -// GetAllSprints will return all sprints from a board, for a given board Id. +// DeleteBoard wraps DeleteBoardWithContext using the background context. +// Caller must close resp.Body +func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) { + return s.DeleteBoardWithContext(context.Background(), boardID) +} + +// GetAllSprintsWithContext will return all sprints from a board, for a given board Id. // This only includes sprints that the user has permission to view. // -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint -func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint +func (s *BoardService) GetAllSprintsWithContext(ctx context.Context, boardID string) ([]Sprint, *Response, error) { id, err := strconv.Atoi(boardID) if err != nil { return nil, nil, err @@ -229,17 +254,22 @@ func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error return result.Values, response, nil } -// GetAllSprintsWithOptions will return sprints from a board, for a given board Id and filtering options +// GetAllSprints wraps GetAllSprintsWithContext using the background context. +func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error) { + return s.GetAllSprintsWithContext(context.Background(), boardID) +} + +// GetAllSprintsWithOptionsWithContext will return sprints from a board, for a given board Id and filtering options // This only includes sprints that the user has permission to view. // -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint -func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint +func (s *BoardService) GetAllSprintsWithOptionsWithContext(ctx context.Context, boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/sprint", boardID) url, err := addOptions(apiEndpoint, options) if err != nil { return nil, nil, err } - req, err := s.client.NewRequest("GET", url, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, nil, err } @@ -253,12 +283,17 @@ func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSpri return result, resp, err } -// GetBoardConfiguration will return a board configuration for a given board Id +// GetAllSprintsWithOptions wraps GetAllSprintsWithOptionsWithContext using the background context. +func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) { + return s.GetAllSprintsWithOptionsWithContext(context.Background(), boardID, options) +} + +// GetBoardConfigurationWithContext will return a board configuration for a given board Id // Jira API docs:https://developer.atlassian.com/cloud/jira/software/rest/#api-rest-agile-1-0-board-boardId-configuration-get -func (s *BoardService) GetBoardConfiguration(boardID int) (*BoardConfiguration, *Response, error) { +func (s *BoardService) GetBoardConfigurationWithContext(ctx context.Context, boardID int) (*BoardConfiguration, *Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/configuration", boardID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err @@ -273,3 +308,8 @@ func (s *BoardService) GetBoardConfiguration(boardID int) (*BoardConfiguration, return result, resp, err } + +// GetBoardConfiguration wraps GetBoardConfigurationWithContext using the background context. +func (s *BoardService) GetBoardConfiguration(boardID int) (*BoardConfiguration, *Response, error) { + return s.GetBoardConfigurationWithContext(context.Background(), boardID) +} diff --git a/vendor/github.com/andygrunwald/go-jira/component.go b/vendor/github.com/andygrunwald/go-jira/component.go index 407ad36a..b76fe0cf 100644 --- a/vendor/github.com/andygrunwald/go-jira/component.go +++ b/vendor/github.com/andygrunwald/go-jira/component.go @@ -1,13 +1,14 @@ package jira -// ComponentService handles components for the JIRA instance / API. -// -// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.1/#api/2/component +import "context" + +// ComponentService handles components for the Jira instance / API.// +// Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.1/#api/2/component type ComponentService struct { client *Client } -// CreateComponentOptions are passed to the ComponentService.Create function to create a new JIRA component +// CreateComponentOptions are passed to the ComponentService.Create function to create a new Jira component type CreateComponentOptions struct { Name string `json:"name,omitempty" structs:"name,omitempty"` Description string `json:"description,omitempty" structs:"description,omitempty"` @@ -19,10 +20,10 @@ type CreateComponentOptions struct { ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` } -// Create creates a new JIRA component based on the given options. -func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComponent, *Response, error) { +// CreateWithContext creates a new Jira component based on the given options. +func (s *ComponentService) CreateWithContext(ctx context.Context, options *CreateComponentOptions) (*ProjectComponent, *Response, error) { apiEndpoint := "rest/api/2/component" - req, err := s.client.NewRequest("POST", apiEndpoint, options) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, options) if err != nil { return nil, nil, err } @@ -36,3 +37,8 @@ func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComp return component, resp, nil } + +// Create wraps CreateWithContext using the background context. +func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComponent, *Response, error) { + return s.CreateWithContext(context.Background(), options) +} diff --git a/vendor/github.com/andygrunwald/go-jira/customer.go b/vendor/github.com/andygrunwald/go-jira/customer.go new file mode 100644 index 00000000..3df31c1c --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/customer.go @@ -0,0 +1,72 @@ +package jira + +import ( + "context" + "net/http" +) + +// CustomerService handles ServiceDesk customers for the Jira instance / API. +type CustomerService struct { + client *Client +} + +// Customer represents a ServiceDesk customer. +type Customer struct { + AccountID string `json:"accountId,omitempty" structs:"accountId,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + Key string `json:"key,omitempty" structs:"key,omitempty"` + EmailAddress string `json:"emailAddress,omitempty" structs:"emailAddress,omitempty"` + DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"` + Active *bool `json:"active,omitempty" structs:"active,omitempty"` + TimeZone string `json:"timeZone,omitempty" structs:"timeZone,omitempty"` + Links *SelfLink `json:"_links,omitempty" structs:"_links,omitempty"` +} + +// CustomerListOptions is the query options for listing customers. +type CustomerListOptions struct { + Query string `url:"query,omitempty"` + Start int `url:"start,omitempty"` + Limit int `url:"limit,omitempty"` +} + +// CustomerList is a page of customers. +type CustomerList struct { + Values []Customer `json:"values,omitempty" structs:"values,omitempty"` + Start int `json:"start,omitempty" structs:"start,omitempty"` + Limit int `json:"limit,omitempty" structs:"limit,omitempty"` + IsLast bool `json:"isLastPage,omitempty" structs:"isLastPage,omitempty"` + Expands []string `json:"_expands,omitempty" structs:"_expands,omitempty"` +} + +// CreateWithContext creates a ServiceDesk customer. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-customer/#api-rest-servicedeskapi-customer-post +func (c *CustomerService) CreateWithContext(ctx context.Context, email, displayName string) (*Customer, *Response, error) { + const apiEndpoint = "rest/servicedeskapi/customer" + + payload := struct { + Email string `json:"email"` + DisplayName string `json:"displayName"` + }{ + Email: email, + DisplayName: displayName, + } + + req, err := c.client.NewRequestWithContext(ctx, http.MethodPost, apiEndpoint, payload) + if err != nil { + return nil, nil, err + } + + responseCustomer := new(Customer) + resp, err := c.client.Do(req, responseCustomer) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + + return responseCustomer, resp, nil +} + +// Create wraps CreateWithContext using the background context. +func (c *CustomerService) Create(email, displayName string) (*Customer, *Response, error) { + return c.CreateWithContext(context.Background(), email, displayName) +} diff --git a/vendor/github.com/andygrunwald/go-jira/error.go b/vendor/github.com/andygrunwald/go-jira/error.go index bd1a5b9a..c7bc2e58 100644 --- a/vendor/github.com/andygrunwald/go-jira/error.go +++ b/vendor/github.com/andygrunwald/go-jira/error.go @@ -10,7 +10,7 @@ import ( "github.com/pkg/errors" ) -// Error message from JIRA +// Error message from Jira // See https://docs.atlassian.com/jira/REST/cloud/#error-responses type Error struct { HTTPError error @@ -34,13 +34,13 @@ func NewJiraError(resp *Response, httpError error) error { if strings.HasPrefix(contentType, "application/json") { err = json.Unmarshal(body, &jerr) if err != nil { - httpError = errors.Wrap(errors.New("Could not parse JSON"), httpError.Error()) + httpError = errors.Wrap(errors.New("could not parse JSON"), httpError.Error()) return errors.Wrap(err, httpError.Error()) } } else { - if httpError == nil { - return fmt.Errorf("Got Response Status %s:%s", resp.Status, string(body)) - } + if httpError == nil { + return fmt.Errorf("got response status %s:%s", resp.Status, string(body)) + } return errors.Wrap(httpError, fmt.Sprintf("%s: %s", resp.Status, string(body))) } diff --git a/vendor/github.com/andygrunwald/go-jira/field.go b/vendor/github.com/andygrunwald/go-jira/field.go index 257d4f99..b14057d9 100644 --- a/vendor/github.com/andygrunwald/go-jira/field.go +++ b/vendor/github.com/andygrunwald/go-jira/field.go @@ -1,13 +1,15 @@ package jira -// FieldService handles fields for the JIRA instance / API. +import "context" + +// FieldService handles fields for the Jira instance / API. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Field +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Field type FieldService struct { client *Client } -// Field represents a field of a JIRA issue. +// Field represents a field of a Jira issue. type Field struct { ID string `json:"id,omitempty" structs:"id,omitempty"` Key string `json:"key,omitempty" structs:"key,omitempty"` @@ -19,17 +21,22 @@ type Field struct { Schema FieldSchema `json:"schema,omitempty" structs:"schema,omitempty"` } +// FieldSchema represents a schema of a Jira field. +// Documentation: https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-fields/#api-rest-api-2-field-get type FieldSchema struct { - Type string `json:"type,omitempty" structs:"type,omitempty"` - System string `json:"system,omitempty" structs:"system,omitempty"` + Type string `json:"type,omitempty" structs:"type,omitempty"` + Items string `json:"items,omitempty" structs:"items,omitempty"` + Custom string `json:"custom,omitempty" structs:"custom,omitempty"` + System string `json:"system,omitempty" structs:"system,omitempty"` + CustomID int64 `json:"customId,omitempty" structs:"customId,omitempty"` } -// GetList gets all fields from JIRA +// GetListWithContext gets all fields from Jira // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-field-get -func (s *FieldService) GetList() ([]Field, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-field-get +func (s *FieldService) GetListWithContext(ctx context.Context) ([]Field, *Response, error) { apiEndpoint := "rest/api/2/field" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -41,3 +48,8 @@ func (s *FieldService) GetList() ([]Field, *Response, error) { } return fieldList, resp, nil } + +// GetList wraps GetListWithContext using the background context. +func (s *FieldService) GetList() ([]Field, *Response, error) { + return s.GetListWithContext(context.Background()) +} diff --git a/vendor/github.com/andygrunwald/go-jira/filter.go b/vendor/github.com/andygrunwald/go-jira/filter.go index 666153aa..f40f3a58 100644 --- a/vendor/github.com/andygrunwald/go-jira/filter.go +++ b/vendor/github.com/andygrunwald/go-jira/filter.go @@ -1,11 +1,15 @@ package jira -import "github.com/google/go-querystring/query" -import "fmt" +import ( + "context" + "fmt" -// FilterService handles fields for the JIRA instance / API. + "github.com/google/go-querystring/query" +) + +// FilterService handles fields for the Jira instance / API. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Filter +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Filter type FilterService struct { client *Client } @@ -116,23 +120,21 @@ type FilterSearchOptions struct { Expand string `url:"expand,omitempty"` } -// GetList retrieves all filters from Jira -func (fs *FilterService) GetList() ([]*Filter, *Response, error) { +// GetListWithContext retrieves all filters from Jira +func (fs *FilterService) GetListWithContext(ctx context.Context) ([]*Filter, *Response, error) { options := &GetQueryOptions{} apiEndpoint := "rest/api/2/filter" - req, err := fs.client.NewRequest("GET", apiEndpoint, nil) + req, err := fs.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } - if options != nil { - q, err := query.Values(options) - if err != nil { - return nil, nil, err - } - req.URL.RawQuery = q.Encode() + q, err := query.Values(options) + if err != nil { + return nil, nil, err } + req.URL.RawQuery = q.Encode() filters := []*Filter{} resp, err := fs.client.Do(req, &filters) @@ -143,10 +145,15 @@ func (fs *FilterService) GetList() ([]*Filter, *Response, error) { return filters, resp, err } -// GetFavouriteList retrieves the user's favourited filters from Jira -func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) { +// GetList wraps GetListWithContext using the background context. +func (fs *FilterService) GetList() ([]*Filter, *Response, error) { + return fs.GetListWithContext(context.Background()) +} + +// GetFavouriteListWithContext retrieves the user's favourited filters from Jira +func (fs *FilterService) GetFavouriteListWithContext(ctx context.Context) ([]*Filter, *Response, error) { apiEndpoint := "rest/api/2/filter/favourite" - req, err := fs.client.NewRequest("GET", apiEndpoint, nil) + req, err := fs.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -159,10 +166,15 @@ func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) { return filters, resp, err } -// Get retrieves a single Filter from Jira -func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) { +// GetFavouriteList wraps GetFavouriteListWithContext using the background context. +func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) { + return fs.GetFavouriteListWithContext(context.Background()) +} + +// GetWithContext retrieves a single Filter from Jira +func (fs *FilterService) GetWithContext(ctx context.Context, filterID int) (*Filter, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/filter/%d", filterID) - req, err := fs.client.NewRequest("GET", apiEndpoint, nil) + req, err := fs.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -176,16 +188,21 @@ func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) { return filter, resp, err } -// GetMyFilters retrieves the my Filters. +// Get wraps GetWithContext using the background context. +func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) { + return fs.GetWithContext(context.Background(), filterID) +} + +// GetMyFiltersWithContext retrieves the my Filters. // // https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-filter-my-get -func (fs *FilterService) GetMyFilters(opts *GetMyFiltersQueryOptions) ([]*Filter, *Response, error) { +func (fs *FilterService) GetMyFiltersWithContext(ctx context.Context, opts *GetMyFiltersQueryOptions) ([]*Filter, *Response, error) { apiEndpoint := "rest/api/3/filter/my" url, err := addOptions(apiEndpoint, opts) if err != nil { return nil, nil, err } - req, err := fs.client.NewRequest("GET", url, nil) + req, err := fs.client.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, nil, err } @@ -199,16 +216,21 @@ func (fs *FilterService) GetMyFilters(opts *GetMyFiltersQueryOptions) ([]*Filter return filters, resp, nil } -// Search will search for filter according to the search options +// GetMyFilters wraps GetMyFiltersWithContext using the background context. +func (fs *FilterService) GetMyFilters(opts *GetMyFiltersQueryOptions) ([]*Filter, *Response, error) { + return fs.GetMyFiltersWithContext(context.Background(), opts) +} + +// SearchWithContext will search for filter according to the search options // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-filter-search-get -func (fs *FilterService) Search(opt *FilterSearchOptions) (*FiltersList, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-filter-search-get +func (fs *FilterService) SearchWithContext(ctx context.Context, opt *FilterSearchOptions) (*FiltersList, *Response, error) { apiEndpoint := "rest/api/3/filter/search" url, err := addOptions(apiEndpoint, opt) if err != nil { return nil, nil, err } - req, err := fs.client.NewRequest("GET", url, nil) + req, err := fs.client.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, nil, err } @@ -222,3 +244,8 @@ func (fs *FilterService) Search(opt *FilterSearchOptions) (*FiltersList, *Respon return filters, resp, err } + +// Search wraps SearchWithContext using the background context. +func (fs *FilterService) Search(opt *FilterSearchOptions) (*FiltersList, *Response, error) { + return fs.SearchWithContext(context.Background(), opt) +} diff --git a/vendor/github.com/andygrunwald/go-jira/group.go b/vendor/github.com/andygrunwald/go-jira/group.go index a0b77764..f78c6810 100644 --- a/vendor/github.com/andygrunwald/go-jira/group.go +++ b/vendor/github.com/andygrunwald/go-jira/group.go @@ -1,13 +1,14 @@ package jira import ( + "context" "fmt" "net/url" ) -// GroupService handles Groups for the JIRA instance / API. +// GroupService handles Groups for the Jira instance / API. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group +// Jira API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group type GroupService struct { client *Client } @@ -21,7 +22,7 @@ type groupMembersResult struct { Members []GroupMember `json:"values"` } -// Group represents a JIRA group +// Group represents a Jira group type Group struct { ID string `json:"id"` Title string `json:"title"` @@ -58,16 +59,16 @@ type GroupSearchOptions struct { IncludeInactiveUsers bool } -// Get returns a paginated list of users who are members of the specified group and its subgroups. +// GetWithContext returns a paginated list of users who are members of the specified group and its subgroups. // Users in the page are ordered by user names. // User of this resource is required to have sysadmin or admin permissions. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup +// Jira API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup // // WARNING: This API only returns the first page of group members -func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) { +func (s *GroupService) GetWithContext(ctx context.Context, name string) ([]GroupMember, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name)) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -81,12 +82,17 @@ func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) { return group.Members, resp, nil } -// GetWithOptions returns a paginated list of members of the specified group and its subgroups. +// Get wraps GetWithContext using the background context. +func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) { + return s.GetWithContext(context.Background(), name) +} + +// GetWithOptionsWithContext returns a paginated list of members of the specified group and its subgroups. // Users in the page are ordered by user names. // User of this resource is required to have sysadmin or admin permissions. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup -func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup +func (s *GroupService) GetWithOptionsWithContext(ctx context.Context, name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) { var apiEndpoint string if options == nil { apiEndpoint = fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name)) @@ -99,7 +105,7 @@ func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) options.IncludeInactiveUsers, ) } - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -112,16 +118,21 @@ func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) return group.Members, resp, nil } -// Add adds user to group +// GetWithOptions wraps GetWithOptionsWithContext using the background context. +func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) { + return s.GetWithOptionsWithContext(context.Background(), name, options) +} + +// AddWithContext adds user to group // -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-addUserToGroup -func (s *GroupService) Add(groupname string, username string) (*Group, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-addUserToGroup +func (s *GroupService) AddWithContext(ctx context.Context, groupname string, username string) (*Group, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s", groupname) var user struct { Name string `json:"name"` } user.Name = username - req, err := s.client.NewRequest("POST", apiEndpoint, &user) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, &user) if err != nil { return nil, nil, err } @@ -136,12 +147,18 @@ func (s *GroupService) Add(groupname string, username string) (*Group, *Response return responseGroup, resp, nil } -// Remove removes user from group +// Add wraps AddWithContext using the background context. +func (s *GroupService) Add(groupname string, username string) (*Group, *Response, error) { + return s.AddWithContext(context.Background(), groupname, username) +} + +// RemoveWithContext removes user from group // -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-removeUserFromGroup -func (s *GroupService) Remove(groupname string, username string) (*Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-removeUserFromGroup +// Caller must close resp.Body +func (s *GroupService) RemoveWithContext(ctx context.Context, groupname string, username string) (*Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s&username=%s", groupname, username) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { return nil, err } @@ -154,3 +171,9 @@ func (s *GroupService) Remove(groupname string, username string) (*Response, err return resp, nil } + +// Remove wraps RemoveWithContext using the background context. +// Caller must close resp.Body +func (s *GroupService) Remove(groupname string, username string) (*Response, error) { + return s.RemoveWithContext(context.Background(), groupname, username) +} diff --git a/vendor/github.com/andygrunwald/go-jira/issue.go b/vendor/github.com/andygrunwald/go-jira/issue.go index 857373cc..0aa03b70 100644 --- a/vendor/github.com/andygrunwald/go-jira/issue.go +++ b/vendor/github.com/andygrunwald/go-jira/issue.go @@ -2,6 +2,7 @@ package jira import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -10,6 +11,7 @@ import ( "net/http" "net/url" "reflect" + "strconv" "strings" "time" @@ -19,13 +21,13 @@ import ( ) const ( - // AssigneeAutomatic represents the value of the "Assignee: Automatic" of JIRA + // AssigneeAutomatic represents the value of the "Assignee: Automatic" of Jira AssigneeAutomatic = "-1" ) -// IssueService handles Issues for the JIRA instance / API. +// IssueService handles Issues for the Jira instance / API. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue type IssueService struct { client *Client } @@ -37,7 +39,7 @@ type UpdateQueryOptions struct { OverrideEditableFlag bool `url:"overrideEditableFlag,omitempty"` } -// Issue represents a JIRA issue. +// Issue represents a Jira issue. type Issue struct { Expand string `json:"expand,omitempty" structs:"expand,omitempty"` ID string `json:"id,omitempty" structs:"id,omitempty"` @@ -47,6 +49,7 @@ type Issue struct { RenderedFields *IssueRenderedFields `json:"renderedFields,omitempty" structs:"renderedFields,omitempty"` Changelog *Changelog `json:"changelog,omitempty" structs:"changelog,omitempty"` Transitions []Transition `json:"transitions,omitempty" structs:"transitions,omitempty"` + Names map[string]string `json:"names,omitempty" structs:"names,omitempty"` } // ChangelogItems reflects one single changelog item of a history item @@ -72,7 +75,7 @@ type Changelog struct { Histories []ChangelogHistory `json:"histories,omitempty"` } -// Attachment represents a JIRA attachment +// Attachment represents a Jira attachment type Attachment struct { Self string `json:"self,omitempty" structs:"self,omitempty"` ID string `json:"id,omitempty" structs:"id,omitempty"` @@ -96,8 +99,8 @@ type Epic struct { Done bool `json:"done" structs:"done"` } -// IssueFields represents single fields of a JIRA issue. -// Every JIRA issue has several fields attached. +// IssueFields represents single fields of a Jira issue. +// Every Jira issue has several fields attached. type IssueFields struct { // TODO Missing fields // * "workratio": -1, @@ -106,6 +109,7 @@ type IssueFields struct { Expand string `json:"expand,omitempty" structs:"expand,omitempty"` Type IssueType `json:"issuetype,omitempty" structs:"issuetype,omitempty"` Project Project `json:"project,omitempty" structs:"project,omitempty"` + Environment string `json:"environment,omitempty" structs:"environment,omitempty"` Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"` Priority *Priority `json:"priority,omitempty" structs:"priority,omitempty"` Resolutiondate Time `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"` @@ -144,7 +148,7 @@ type IssueFields struct { } // MarshalJSON is a custom JSON marshal function for the IssueFields structs. -// It handles JIRA custom fields and maps those from / to "Unknowns" key. +// It handles Jira custom fields and maps those from / to "Unknowns" key. func (i *IssueFields) MarshalJSON() ([]byte, error) { m := structs.Map(i) unknowns, okay := m["Unknowns"] @@ -159,7 +163,7 @@ func (i *IssueFields) MarshalJSON() ([]byte, error) { } // UnmarshalJSON is a custom JSON marshal function for the IssueFields structs. -// It handles JIRA custom fields and maps those from / to "Unknowns" key. +// It handles Jira custom fields and maps those from / to "Unknowns" key. func (i *IssueFields) UnmarshalJSON(data []byte) error { // Do the normal unmarshalling first @@ -191,7 +195,7 @@ func (i *IssueFields) UnmarshalJSON(data []byte) error { options := strings.Split(tagDetail, ",") if len(options) == 0 { - return fmt.Errorf("No tags options found for %s", field.Name) + return fmt.Errorf("no tags options found for %s", field.Name) } // the first one is the json tag key := options[0] @@ -207,7 +211,7 @@ func (i *IssueFields) UnmarshalJSON(data []byte) error { } -// IssueRenderedFields represents rendered fields of a JIRA issue. +// IssueRenderedFields represents rendered fields of a Jira issue. // Not all IssueFields are rendered. type IssueRenderedFields struct { // TODO Missing fields @@ -225,7 +229,7 @@ type IssueRenderedFields struct { Description string `json:"description,omitempty" structs:"description,omitempty"` } -// IssueType represents a type of a JIRA issue. +// IssueType represents a type of a Jira issue. // Typical types are "Request", "Bug", "Story", ... type IssueType struct { Self string `json:"self,omitempty" structs:"self,omitempty"` @@ -237,7 +241,7 @@ type IssueType struct { AvatarID int `json:"avatarId,omitempty" structs:"avatarId,omitempty"` } -// Watches represents a type of how many and which user are "observing" a JIRA issue to track the status / updates. +// Watches represents a type of how many and which user are "observing" a Jira issue to track the status / updates. type Watches struct { Self string `json:"self,omitempty" structs:"self,omitempty"` WatchCount int `json:"watchCount,omitempty" structs:"watchCount,omitempty"` @@ -249,6 +253,7 @@ type Watches struct { type Watcher struct { Self string `json:"self,omitempty" structs:"self,omitempty"` Name string `json:"name,omitempty" structs:"name,omitempty"` + AccountID string `json:"accountId,omitempty" structs:"accountId,omitempty"` DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"` Active bool `json:"active,omitempty" structs:"active,omitempty"` } @@ -261,35 +266,36 @@ type AvatarUrls struct { Three2X32 string `json:"32x32,omitempty" structs:"32x32,omitempty"` } -// Component represents a "component" of a JIRA issue. -// Components can be user defined in every JIRA instance. +// Component represents a "component" of a Jira issue. +// Components can be user defined in every Jira instance. type Component struct { - Self string `json:"self,omitempty" structs:"self,omitempty"` - ID string `json:"id,omitempty" structs:"id,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + Description string `json:"description,omitempty" structs:"description,omitempty"` } -// Progress represents the progress of a JIRA issue. +// Progress represents the progress of a Jira issue. type Progress struct { Progress int `json:"progress" structs:"progress"` Total int `json:"total" structs:"total"` Percent int `json:"percent" structs:"percent"` } -// Parent represents the parent of a JIRA issue, to be used with subtask issue types. +// Parent represents the parent of a Jira issue, to be used with subtask issue types. type Parent struct { - ID string `json:"id,omitempty" structs:"id"` - Key string `json:"key,omitempty" structs:"key"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Key string `json:"key,omitempty" structs:"key,omitempty"` } -// Time represents the Time definition of JIRA as a time.Time of go +// Time represents the Time definition of Jira as a time.Time of go type Time time.Time func (t Time) Equal(u Time) bool { return time.Time(t).Equal(time.Time(u)) } -// Date represents the Date definition of JIRA as a time.Time of go +// Date represents the Date definition of Jira as a time.Time of go type Date time.Time // Wrapper struct for search result @@ -297,7 +303,7 @@ type transitionResult struct { Transitions []Transition `json:"transitions" structs:"transitions"` } -// Transition represents an issue transition in JIRA +// Transition represents an issue transition in Jira type Transition struct { ID string `json:"id" structs:"id"` Name string `json:"name" structs:"name"` @@ -312,10 +318,26 @@ type TransitionField struct { // CreateTransitionPayload is used for creating new issue transitions type CreateTransitionPayload struct { + Update TransitionPayloadUpdate `json:"update,omitempty" structs:"update,omitempty"` Transition TransitionPayload `json:"transition" structs:"transition"` Fields TransitionPayloadFields `json:"fields" structs:"fields"` } +// TransitionPayloadUpdate represents the updates of Transition calls like DoTransition +type TransitionPayloadUpdate struct { + Comment []TransitionPayloadComment `json:"comment,omitempty" structs:"comment,omitempty"` +} + +// TransitionPayloadComment represents comment in Transition payload +type TransitionPayloadComment struct { + Add TransitionPayloadCommentBody `json:"add,omitempty" structs:"add,omitempty"` +} + +// TransitionPayloadCommentBody represents body of comment in payload +type TransitionPayloadCommentBody struct { + Body string `json:"body,omitempty"` +} + // TransitionPayload represents the request payload of Transition calls like DoTransition type TransitionPayload struct { ID string `json:"id" structs:"id"` @@ -332,8 +354,8 @@ type Option struct { Value string `json:"value" structs:"value"` } -// UnmarshalJSON will transform the JIRA time into a time.Time -// during the transformation of the JIRA JSON response +// UnmarshalJSON will transform the Jira time into a time.Time +// during the transformation of the Jira JSON response func (t *Time) UnmarshalJSON(b []byte) error { // Ignore null, like in the main JSON package. if string(b) == "null" { @@ -347,14 +369,14 @@ func (t *Time) UnmarshalJSON(b []byte) error { return nil } -// MarshalJSON will transform the time.Time into a JIRA time -// during the creation of a JIRA request +// MarshalJSON will transform the time.Time into a Jira time +// during the creation of a Jira request func (t Time) MarshalJSON() ([]byte, error) { - return []byte(time.Time(t).Format("\"2006-01-02T15:04:05.999-0700\"")), nil + return []byte(time.Time(t).Format("\"2006-01-02T15:04:05.000-0700\"")), nil } -// UnmarshalJSON will transform the JIRA date into a time.Time -// during the transformation of the JIRA JSON response +// UnmarshalJSON will transform the Jira date into a time.Time +// during the transformation of the Jira JSON response func (t *Date) UnmarshalJSON(b []byte) error { // Ignore null, like in the main JSON package. if string(b) == "null" { @@ -369,16 +391,16 @@ func (t *Date) UnmarshalJSON(b []byte) error { } // MarshalJSON will transform the Date object into a short -// date string as JIRA expects during the creation of a -// JIRA request +// date string as Jira expects during the creation of a +// Jira request func (t Date) MarshalJSON() ([]byte, error) { time := time.Time(t) return []byte(time.Format("\"2006-01-02\"")), nil } -// Worklog represents the work log of a JIRA issue. +// Worklog represents the work log of a Jira issue. // One Worklog contains zero or n WorklogRecords -// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html +// Jira Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html type Worklog struct { StartAt int `json:"startAt" structs:"startAt"` MaxResults int `json:"maxResults" structs:"maxResults"` @@ -407,7 +429,7 @@ type EntityProperty struct { Value interface{} `json:"value"` } -// TimeTracking represents the timetracking fields of a JIRA issue. +// TimeTracking represents the timetracking fields of a Jira issue. type TimeTracking struct { OriginalEstimate string `json:"originalEstimate,omitempty" structs:"originalEstimate,omitempty"` RemainingEstimate string `json:"remainingEstimate,omitempty" structs:"remainingEstimate,omitempty"` @@ -425,7 +447,7 @@ type Subtasks struct { Fields IssueFields `json:"fields" structs:"fields"` } -// IssueLink represents a link between two issues in JIRA. +// IssueLink represents a link between two issues in Jira. type IssueLink struct { ID string `json:"id,omitempty" structs:"id,omitempty"` Self string `json:"self,omitempty" structs:"self,omitempty"` @@ -435,7 +457,7 @@ type IssueLink struct { Comment *Comment `json:"comment,omitempty" structs:"comment,omitempty"` } -// IssueLinkType represents a type of a link between to issues in JIRA. +// IssueLinkType represents a type of a link between to issues in Jira. // Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc. type IssueLinkType struct { ID string `json:"id,omitempty" structs:"id,omitempty"` @@ -450,7 +472,7 @@ type Comments struct { Comments []*Comment `json:"comments,omitempty" structs:"comments,omitempty"` } -// Comment represents a comment by a person to an issue in JIRA. +// Comment represents a comment by a person to an issue in Jira. type Comment struct { ID string `json:"id,omitempty" structs:"id,omitempty"` Self string `json:"self,omitempty" structs:"self,omitempty"` @@ -489,7 +511,7 @@ type CommentVisibility struct { // SearchOptions specifies the optional parameters to various List methods that // support pagination. -// Pagination is used for the JIRA REST APIs to conserve server resources and limit +// Pagination is used for the Jira REST APIs to conserve server resources and limit // response size for resources that return potentially large collection of items. // A request to a pages API will result in a values array wrapped in a JSON object with some paging metadata // Default Pagination options @@ -529,9 +551,10 @@ type GetQueryOptions struct { // GetWorklogsQueryOptions specifies the optional parameters for the Get Worklogs method type GetWorklogsQueryOptions struct { - StartAt int64 `url:"startAt,omitempty"` - MaxResults int32 `url:"maxResults,omitempty"` - Expand string `url:"expand,omitempty"` + StartAt int64 `url:"startAt,omitempty"` + MaxResults int32 `url:"maxResults,omitempty"` + StartedAfter int64 `url:"startedAfter,omitempty"` + Expand string `url:"expand,omitempty"` } type AddWorklogQueryOptions struct { @@ -543,8 +566,8 @@ type AddWorklogQueryOptions struct { OverrideEditableFlag bool `url:"overrideEditableFlag,omitempty"` } -// CustomFields represents custom fields of JIRA -// This can heavily differ between JIRA instances +// CustomFields represents custom fields of Jira +// This can heavily differ between Jira instances type CustomFields map[string]string // RemoteLink represents remote links which linked to issues @@ -581,21 +604,21 @@ type RemoteLinkIcon struct { // RemoteLinkStatus if the link is a resolvable object (issue, epic) - the structure represent its status type RemoteLinkStatus struct { - Resolved bool - Icon *RemoteLinkIcon + Resolved bool `json:"resolved,omitempty" structs:"resolved,omitempty"` + Icon *RemoteLinkIcon `json:"icon,omitempty" structs:"icon,omitempty"` } -// Get returns a full representation of the issue for the given issue key. -// JIRA will attempt to identify the issue by the issueIdOrKey path parameter. +// GetWithContext returns a full representation of the issue for the given issue key. +// Jira will attempt to identify the issue by the issueIdOrKey path parameter. // This can be an issue id, or an issue key. -// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved. +// If the issue cannot be found via an exact match, Jira will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved. // // The given options will be appended to the query string // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getIssue -func (s *IssueService) Get(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getIssue +func (s *IssueService) GetWithContext(ctx context.Context, issueID string, options *GetQueryOptions) (*Issue, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -618,13 +641,18 @@ func (s *IssueService) Get(issueID string, options *GetQueryOptions) (*Issue, *R return issue, resp, nil } -// DownloadAttachment returns a Response of an attachment for a given attachmentID. +// Get wraps GetWithContext using the background context. +func (s *IssueService) Get(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { + return s.GetWithContext(context.Background(), issueID, options) +} + +// DownloadAttachmentWithContext returns a Response of an attachment for a given attachmentID. // The attachment is in the Response.Body of the response. // This is an io.ReadCloser. -// The caller should close the resp.Body. -func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error) { +// Caller must close resp.Body. +func (s *IssueService) DownloadAttachmentWithContext(ctx context.Context, attachmentID string) (*Response, error) { apiEndpoint := fmt.Sprintf("secure/attachment/%s/", attachmentID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, err } @@ -638,8 +666,14 @@ func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error return resp, nil } -// PostAttachment uploads r (io.Reader) as an attachment to a given issueID -func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) { +// DownloadAttachment wraps DownloadAttachmentWithContext using the background context. +// Caller must close resp.Body +func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error) { + return s.DownloadAttachmentWithContext(context.Background(), attachmentID) +} + +// PostAttachmentWithContext uploads r (io.Reader) as an attachment to a given issueID +func (s *IssueService) PostAttachmentWithContext(ctx context.Context, issueID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/attachments", issueID) b := new(bytes.Buffer) @@ -658,7 +692,7 @@ func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentNam } writer.Close() - req, err := s.client.NewMultiPartRequest("POST", apiEndpoint, b) + req, err := s.client.NewMultiPartRequestWithContext(ctx, "POST", apiEndpoint, b) if err != nil { return nil, nil, err } @@ -676,11 +710,17 @@ func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentNam return attachment, resp, nil } -// DeleteAttachment deletes an attachment of a given attachmentID -func (s *IssueService) DeleteAttachment(attachmentID string) (*Response, error) { +// PostAttachment wraps PostAttachmentWithContext using the background context. +func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) { + return s.PostAttachmentWithContext(context.Background(), issueID, r, attachmentName) +} + +// DeleteAttachmentWithContext deletes an attachment of a given attachmentID +// Caller must close resp.Body +func (s *IssueService) DeleteAttachmentWithContext(ctx context.Context, attachmentID string) (*Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/attachment/%s", attachmentID) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { return nil, err } @@ -694,14 +734,45 @@ func (s *IssueService) DeleteAttachment(attachmentID string) (*Response, error) return resp, nil } -// GetWorklogs gets all the worklogs for an issue. +// DeleteAttachment wraps DeleteAttachmentWithContext using the background context. +// Caller must close resp.Body +func (s *IssueService) DeleteAttachment(attachmentID string) (*Response, error) { + return s.DeleteAttachmentWithContext(context.Background(), attachmentID) +} + +// DeleteLinkWithContext deletes a link of a given linkID +// Caller must close resp.Body +func (s *IssueService) DeleteLinkWithContext(ctx context.Context, linkID string) (*Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issueLink/%s", linkID) + + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// DeleteLink wraps DeleteLinkWithContext using the background context. +// Caller must close resp.Body +func (s *IssueService) DeleteLink(linkID string) (*Response, error) { + return s.DeleteLinkWithContext(context.Background(), linkID) +} + +// GetWorklogsWithContext gets all the worklogs for an issue. // This method is especially important if you need to read all the worklogs, not just the first page. // // https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/worklog-getIssueWorklog -func (s *IssueService) GetWorklogs(issueID string, options ...func(*http.Request) error) (*Worklog, *Response, error) { +func (s *IssueService) GetWorklogsWithContext(ctx context.Context, issueID string, options ...func(*http.Request) error) (*Worklog, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog", issueID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -718,6 +789,11 @@ func (s *IssueService) GetWorklogs(issueID string, options ...func(*http.Request return v, resp, err } +// GetWorklogs wraps GetWorklogsWithContext using the background context. +func (s *IssueService) GetWorklogs(issueID string, options ...func(*http.Request) error) (*Worklog, *Response, error) { + return s.GetWorklogsWithContext(context.Background(), issueID, options...) +} + // Applies query options to http request. // This helper is meant to be used with all "QueryOptions" structs. func WithQueryOptions(options interface{}) func(*http.Request) error { @@ -734,14 +810,14 @@ func WithQueryOptions(options interface{}) func(*http.Request) error { } } -// Create creates an issue or a sub-task from a JSON representation. +// CreateWithContext creates an issue or a sub-task from a JSON representation. // Creating a sub-task is similar to creating a regular issue, with two important differences: // The issueType field must correspond to a sub-task issue type and you must provide a parent field in the issue create request containing the id or key of the parent issue. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-createIssues -func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-createIssues +func (s *IssueService) CreateWithContext(ctx context.Context, issue *Issue) (*Issue, *Response, error) { apiEndpoint := "rest/api/2/issue" - req, err := s.client.NewRequest("POST", apiEndpoint, issue) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, issue) if err != nil { return nil, nil, err } @@ -755,26 +831,32 @@ func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) { defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, resp, fmt.Errorf("Could not read the returned data") + return nil, resp, fmt.Errorf("could not read the returned data") } err = json.Unmarshal(data, responseIssue) if err != nil { - return nil, resp, fmt.Errorf("Could not unmarshall the data into struct") + return nil, resp, fmt.Errorf("could not unmarshall the data into struct") } return responseIssue, resp, nil } -// UpdateWithOptions updates an issue from a JSON representation, -// while also specifiying query params. The issue is found by key. +// Create wraps CreateWithContext using the background context. +func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) { + return s.CreateWithContext(context.Background(), issue) +} + +// UpdateWithOptionsWithContext updates an issue from a JSON representation, +// while also specifying query params. The issue is found by key. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue -func (s *IssueService) UpdateWithOptions(issue *Issue, opts *UpdateQueryOptions) (*Issue, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue +// Caller must close resp.Body +func (s *IssueService) UpdateWithOptionsWithContext(ctx context.Context, issue *Issue, opts *UpdateQueryOptions) (*Issue, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", issue.Key) url, err := addOptions(apiEndpoint, opts) if err != nil { return nil, nil, err } - req, err := s.client.NewRequest("PUT", url, issue) + req, err := s.client.NewRequestWithContext(ctx, "PUT", url, issue) if err != nil { return nil, nil, err } @@ -790,19 +872,31 @@ func (s *IssueService) UpdateWithOptions(issue *Issue, opts *UpdateQueryOptions) return &ret, resp, nil } -// Update updates an issue from a JSON representation. The issue is found by key. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue -func (s *IssueService) Update(issue *Issue) (*Issue, *Response, error) { - return s.UpdateWithOptions(issue, nil) +// UpdateWithOptions wraps UpdateWithOptionsWithContext using the background context. +// Caller must close resp.Body +func (s *IssueService) UpdateWithOptions(issue *Issue, opts *UpdateQueryOptions) (*Issue, *Response, error) { + return s.UpdateWithOptionsWithContext(context.Background(), issue, opts) } -// UpdateIssue updates an issue from a JSON representation. The issue is found by key. +// UpdateWithContext updates an issue from a JSON representation. The issue is found by key. +// +// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue +func (s *IssueService) UpdateWithContext(ctx context.Context, issue *Issue) (*Issue, *Response, error) { + return s.UpdateWithOptionsWithContext(ctx, issue, nil) +} + +// Update wraps UpdateWithContext using the background context. +func (s *IssueService) Update(issue *Issue) (*Issue, *Response, error) { + return s.UpdateWithContext(context.Background(), issue) +} + +// UpdateIssueWithContext updates an issue from a JSON representation. The issue is found by key. // // https://docs.atlassian.com/jira/REST/7.4.0/#api/2/issue-editIssue -func (s *IssueService) UpdateIssue(jiraID string, data map[string]interface{}) (*Response, error) { +// Caller must close resp.Body +func (s *IssueService) UpdateIssueWithContext(ctx context.Context, jiraID string, data map[string]interface{}) (*Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", jiraID) - req, err := s.client.NewRequest("PUT", apiEndpoint, data) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, data) if err != nil { return nil, err } @@ -816,12 +910,18 @@ func (s *IssueService) UpdateIssue(jiraID string, data map[string]interface{}) ( return resp, nil } -// AddComment adds a new comment to issueID. +// UpdateIssue wraps UpdateIssueWithContext using the background context. +// Caller must close resp.Body +func (s *IssueService) UpdateIssue(jiraID string, data map[string]interface{}) (*Response, error) { + return s.UpdateIssueWithContext(context.Background(), jiraID, data) +} + +// AddCommentWithContext adds a new comment to issueID. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-addComment -func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-addComment +func (s *IssueService) AddCommentWithContext(ctx context.Context, issueID string, comment *Comment) (*Comment, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment", issueID) - req, err := s.client.NewRequest("POST", apiEndpoint, comment) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, comment) if err != nil { return nil, nil, err } @@ -836,17 +936,22 @@ func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, * return responseComment, resp, nil } -// UpdateComment updates the body of a comment, identified by comment.ID, on the issueID. +// AddComment wraps AddCommentWithContext using the background context. +func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *Response, error) { + return s.AddCommentWithContext(context.Background(), issueID, comment) +} + +// UpdateCommentWithContext updates the body of a comment, identified by comment.ID, on the issueID. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/comment-updateComment -func (s *IssueService) UpdateComment(issueID string, comment *Comment) (*Comment, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/comment-updateComment +func (s *IssueService) UpdateCommentWithContext(ctx context.Context, issueID string, comment *Comment) (*Comment, *Response, error) { reqBody := struct { Body string `json:"body"` }{ Body: comment.Body, } apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment/%s", issueID, comment.ID) - req, err := s.client.NewRequest("PUT", apiEndpoint, reqBody) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, reqBody) if err != nil { return nil, nil, err } @@ -860,12 +965,17 @@ func (s *IssueService) UpdateComment(issueID string, comment *Comment) (*Comment return responseComment, resp, nil } -// DeleteComment Deletes a comment from an issueID. +// UpdateComment wraps UpdateCommentWithContext using the background context. +func (s *IssueService) UpdateComment(issueID string, comment *Comment) (*Comment, *Response, error) { + return s.UpdateCommentWithContext(context.Background(), issueID, comment) +} + +// DeleteCommentWithContext Deletes a comment from an issueID. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-issue-issueIdOrKey-comment-id-delete -func (s *IssueService) DeleteComment(issueID, commentID string) error { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-issue-issueIdOrKey-comment-id-delete +func (s *IssueService) DeleteCommentWithContext(ctx context.Context, issueID, commentID string) error { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment/%s", issueID, commentID) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { return err } @@ -875,16 +985,22 @@ func (s *IssueService) DeleteComment(issueID, commentID string) error { jerr := NewJiraError(resp, err) return jerr } + defer resp.Body.Close() return nil } -// AddWorklogRecord adds a new worklog record to issueID. +// DeleteComment wraps DeleteCommentWithContext using the background context. +func (s *IssueService) DeleteComment(issueID, commentID string) error { + return s.DeleteCommentWithContext(context.Background(), issueID, commentID) +} + +// AddWorklogRecordWithContext adds a new worklog record to issueID. // // https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-issue-issueIdOrKey-worklog-post -func (s *IssueService) AddWorklogRecord(issueID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) { +func (s *IssueService) AddWorklogRecordWithContext(ctx context.Context, issueID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog", issueID) - req, err := s.client.NewRequest("POST", apiEndpoint, record) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, record) if err != nil { return nil, nil, err } @@ -906,12 +1022,17 @@ func (s *IssueService) AddWorklogRecord(issueID string, record *WorklogRecord, o return responseRecord, resp, nil } -// UpdateWorklogRecord updates a worklog record. +// AddWorklogRecord wraps AddWorklogRecordWithContext using the background context. +func (s *IssueService) AddWorklogRecord(issueID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) { + return s.AddWorklogRecordWithContext(context.Background(), issueID, record, options...) +} + +// UpdateWorklogRecordWithContext updates a worklog record. // // https://docs.atlassian.com/software/jira/docs/api/REST/7.1.2/#api/2/issue-updateWorklog -func (s *IssueService) UpdateWorklogRecord(issueID, worklogID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) { +func (s *IssueService) UpdateWorklogRecordWithContext(ctx context.Context, issueID, worklogID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog/%s", issueID, worklogID) - req, err := s.client.NewRequest("PUT", apiEndpoint, record) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, record) if err != nil { return nil, nil, err } @@ -933,12 +1054,18 @@ func (s *IssueService) UpdateWorklogRecord(issueID, worklogID string, record *Wo return responseRecord, resp, nil } -// AddLink adds a link between two issues. +// UpdateWorklogRecord wraps UpdateWorklogRecordWithContext using the background context. +func (s *IssueService) UpdateWorklogRecord(issueID, worklogID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) { + return s.UpdateWorklogRecordWithContext(context.Background(), issueID, worklogID, record, options...) +} + +// AddLinkWithContext adds a link between two issues. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issueLink -func (s *IssueService) AddLink(issueLink *IssueLink) (*Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issueLink") - req, err := s.client.NewRequest("POST", apiEndpoint, issueLink) +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issueLink +// Caller must close resp.Body +func (s *IssueService) AddLinkWithContext(ctx context.Context, issueLink *IssueLink) (*Response, error) { + apiEndpoint := "rest/api/2/issueLink" + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, issueLink) if err != nil { return nil, err } @@ -951,33 +1078,45 @@ func (s *IssueService) AddLink(issueLink *IssueLink) (*Response, error) { return resp, err } -// Search will search for tickets according to the jql +// AddLink wraps AddLinkWithContext using the background context. +// Caller must close resp.Body +func (s *IssueService) AddLink(issueLink *IssueLink) (*Response, error) { + return s.AddLinkWithContext(context.Background(), issueLink) +} + +// SearchWithContext will search for tickets according to the jql // -// JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues -func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Response, error) { - var u string - if options == nil { - u = fmt.Sprintf("rest/api/2/search?jql=%s", url.QueryEscape(jql)) - } else { - u = "rest/api/2/search?jql=" + url.QueryEscape(jql) +// Jira API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues +func (s *IssueService) SearchWithContext(ctx context.Context, jql string, options *SearchOptions) ([]Issue, *Response, error) { + u := url.URL{ + Path: "rest/api/2/search", + } + uv := url.Values{} + if jql != "" { + uv.Add("jql", jql) + } + + if options != nil { if options.StartAt != 0 { - u += fmt.Sprintf("&startAt=%d", options.StartAt) + uv.Add("startAt", strconv.Itoa(options.StartAt)) } if options.MaxResults != 0 { - u += fmt.Sprintf("&maxResults=%d", options.MaxResults) + uv.Add("maxResults", strconv.Itoa(options.MaxResults)) } if options.Expand != "" { - u += fmt.Sprintf("&expand=%s", options.Expand) + uv.Add("expand", options.Expand) } if strings.Join(options.Fields, ",") != "" { - u += fmt.Sprintf("&fields=%s", strings.Join(options.Fields, ",")) + uv.Add("fields", strings.Join(options.Fields, ",")) } if options.ValidateQuery != "" { - u += fmt.Sprintf("&validateQuery=%s", options.ValidateQuery) + uv.Add("validateQuery", options.ValidateQuery) } } - req, err := s.client.NewRequest("GET", u, nil) + u.RawQuery = uv.Encode() + + req, err := s.client.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return []Issue{}, nil, err } @@ -990,10 +1129,15 @@ func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Res return v.Issues, resp, err } -// SearchPages will get issues from all pages in a search +// Search wraps SearchWithContext using the background context. +func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Response, error) { + return s.SearchWithContext(context.Background(), jql, options) +} + +// SearchPagesWithContext will get issues from all pages in a search // -// JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues -func (s *IssueService) SearchPages(jql string, options *SearchOptions, f func(Issue) error) error { +// Jira API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues +func (s *IssueService) SearchPagesWithContext(ctx context.Context, jql string, options *SearchOptions, f func(Issue) error) error { if options == nil { options = &SearchOptions{ StartAt: 0, @@ -1005,11 +1149,15 @@ func (s *IssueService) SearchPages(jql string, options *SearchOptions, f func(Is options.MaxResults = 50 } - issues, resp, err := s.Search(jql, options) + issues, resp, err := s.SearchWithContext(ctx, jql, options) if err != nil { return err } + if len(issues) == 0 { + return nil + } + for { for _, issue := range issues { err = f(issue) @@ -1023,17 +1171,22 @@ func (s *IssueService) SearchPages(jql string, options *SearchOptions, f func(Is } options.StartAt += resp.MaxResults - issues, resp, err = s.Search(jql, options) + issues, resp, err = s.SearchWithContext(ctx, jql, options) if err != nil { return err } } } -// GetCustomFields returns a map of customfield_* keys with string values -func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, error) { +// SearchPages wraps SearchPagesWithContext using the background context. +func (s *IssueService) SearchPages(jql string, options *SearchOptions, f func(Issue) error) error { + return s.SearchPagesWithContext(context.Background(), jql, options, f) +} + +// GetCustomFieldsWithContext returns a map of customfield_* keys with string values +func (s *IssueService) GetCustomFieldsWithContext(ctx context.Context, issueID string) (CustomFields, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -1067,13 +1220,18 @@ func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, return cf, resp, nil } -// GetTransitions gets a list of the transitions possible for this issue by the current user, +// GetCustomFields wraps GetCustomFieldsWithContext using the background context. +func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, error) { + return s.GetCustomFieldsWithContext(context.Background(), issueID) +} + +// GetTransitionsWithContext gets a list of the transitions possible for this issue by the current user, // along with fields that are required and their types. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getTransitions -func (s *IssueService) GetTransitions(id string) ([]Transition, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getTransitions +func (s *IssueService) GetTransitionsWithContext(ctx context.Context, id string) ([]Transition, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions?expand=transitions.fields", id) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -1086,27 +1244,38 @@ func (s *IssueService) GetTransitions(id string) ([]Transition, *Response, error return result.Transitions, resp, err } -// DoTransition performs a transition on an issue. +// GetTransitions wraps GetTransitionsWithContext using the background context. +func (s *IssueService) GetTransitions(id string) ([]Transition, *Response, error) { + return s.GetTransitionsWithContext(context.Background(), id) +} + +// DoTransitionWithContext performs a transition on an issue. // When performing the transition you can update or set other issue fields. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition -func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition +func (s *IssueService) DoTransitionWithContext(ctx context.Context, ticketID, transitionID string) (*Response, error) { payload := CreateTransitionPayload{ Transition: TransitionPayload{ ID: transitionID, }, } - return s.DoTransitionWithPayload(ticketID, payload) + return s.DoTransitionWithPayloadWithContext(ctx, ticketID, payload) } -// DoTransitionWithPayload performs a transition on an issue using any payload. +// DoTransition wraps DoTransitionWithContext using the background context. +func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, error) { + return s.DoTransitionWithContext(context.Background(), ticketID, transitionID) +} + +// DoTransitionWithPayloadWithContext performs a transition on an issue using any payload. // When performing the transition you can update or set other issue fields. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition -func (s *IssueService) DoTransitionWithPayload(ticketID, payload interface{}) (*Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition +// Caller must close resp.Body +func (s *IssueService) DoTransitionWithPayloadWithContext(ctx context.Context, ticketID, payload interface{}) (*Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions", ticketID) - req, err := s.client.NewRequest("POST", apiEndpoint, payload) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, payload) if err != nil { return nil, err } @@ -1119,6 +1288,12 @@ func (s *IssueService) DoTransitionWithPayload(ticketID, payload interface{}) (* return resp, err } +// DoTransitionWithPayload wraps DoTransitionWithPayloadWithContext using the background context. +// Caller must close resp.Body +func (s *IssueService) DoTransitionWithPayload(ticketID, payload interface{}) (*Response, error) { + return s.DoTransitionWithPayloadWithContext(context.Background(), ticketID, payload) +} + // InitIssueWithMetaAndFields returns Issue with with values from fieldsConfig properly set. // * metaProject should contain metaInformation about the project where the issue should be created. // * metaIssuetype is the MetaInformation about the Issuetype that needs to be created. @@ -1189,7 +1364,7 @@ func InitIssueWithMetaAndFields(metaProject *MetaProject, metaIssuetype *MetaIss Value: value, } default: - return nil, fmt.Errorf("Unknown issue type encountered: %s for %s", valueType, key) + return nil, fmt.Errorf("unknown issue type encountered: %s for %s", valueType, key) } } @@ -1198,8 +1373,9 @@ func InitIssueWithMetaAndFields(metaProject *MetaProject, metaIssuetype *MetaIss return issue, nil } -// Delete will delete a specified issue. -func (s *IssueService) Delete(issueID string) (*Response, error) { +// DeleteWithContext will delete a specified issue. +// Caller must close resp.Body +func (s *IssueService) DeleteWithContext(ctx context.Context, issueID string) (*Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID) // to enable deletion of subtasks; without this, the request will fail if the issue has subtasks @@ -1207,7 +1383,7 @@ func (s *IssueService) Delete(issueID string) (*Response, error) { deletePayload["deleteSubtasks"] = "true" content, _ := json.Marshal(deletePayload) - req, err := s.client.NewRequest("DELETE", apiEndpoint, content) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, content) if err != nil { return nil, err } @@ -1216,13 +1392,19 @@ func (s *IssueService) Delete(issueID string) (*Response, error) { return resp, err } -// GetWatchers wil return all the users watching/observing the given issue +// Delete wraps DeleteWithContext using the background context. +// Caller must close resp.Body +func (s *IssueService) Delete(issueID string) (*Response, error) { + return s.DeleteWithContext(context.Background(), issueID) +} + +// GetWatchersWithContext wil return all the users watching/observing the given issue // -// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-getIssueWatchers -func (s *IssueService) GetWatchers(issueID string) (*[]User, *Response, error) { +// Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-getIssueWatchers +func (s *IssueService) GetWatchersWithContext(ctx context.Context, issueID string) (*[]User, *Response, error) { watchesAPIEndpoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) - req, err := s.client.NewRequest("GET", watchesAPIEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", watchesAPIEndpoint, nil) if err != nil { return nil, nil, err } @@ -1234,11 +1416,13 @@ func (s *IssueService) GetWatchers(issueID string) (*[]User, *Response, error) { } result := []User{} - user := new(User) for _, watcher := range watches.Watchers { - user, resp, err = s.client.User.Get(watcher.Name) - if err != nil { - return nil, resp, NewJiraError(resp, err) + var user *User + if watcher.AccountID != "" { + user, resp, err = s.client.User.GetByAccountID(watcher.AccountID) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } } result = append(result, *user) } @@ -1246,13 +1430,45 @@ func (s *IssueService) GetWatchers(issueID string) (*[]User, *Response, error) { return &result, resp, nil } -// AddWatcher adds watcher to the given issue +// GetWatchers wraps GetWatchersWithContext using the background context. +func (s *IssueService) GetWatchers(issueID string) (*[]User, *Response, error) { + return s.GetWatchersWithContext(context.Background(), issueID) +} + +// AddWatcherWithContext adds watcher to the given issue // -// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-addWatcher +// Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-addWatcher +// Caller must close resp.Body +func (s *IssueService) AddWatcherWithContext(ctx context.Context, issueID string, userName string) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) + + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, userName) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + err = NewJiraError(resp, err) + } + + return resp, err +} + +// AddWatcher wraps AddWatcherWithContext using the background context. +// Caller must close resp.Body func (s *IssueService) AddWatcher(issueID string, userName string) (*Response, error) { + return s.AddWatcherWithContext(context.Background(), issueID, userName) +} + +// RemoveWatcherWithContext removes given user from given issue +// +// Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-removeWatcher +// Caller must close resp.Body +func (s *IssueService) RemoveWatcherWithContext(ctx context.Context, issueID string, userName string) (*Response, error) { apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) - req, err := s.client.NewRequest("POST", apiEndPoint, userName) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, userName) if err != nil { return nil, err } @@ -1265,32 +1481,20 @@ func (s *IssueService) AddWatcher(issueID string, userName string) (*Response, e return resp, err } -// RemoveWatcher removes given user from given issue -// -// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-removeWatcher +// RemoveWatcher wraps RemoveWatcherWithContext using the background context. +// Caller must close resp.Body func (s *IssueService) RemoveWatcher(issueID string, userName string) (*Response, error) { - apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) - - req, err := s.client.NewRequest("DELETE", apiEndPoint, userName) - if err != nil { - return nil, err - } - - resp, err := s.client.Do(req, nil) - if err != nil { - err = NewJiraError(resp, err) - } - - return resp, err + return s.RemoveWatcherWithContext(context.Background(), issueID, userName) } -// UpdateAssignee updates the user assigned to work on the given issue +// UpdateAssigneeWithContext updates the user assigned to work on the given issue // -// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.2/#api/2/issue-assign -func (s *IssueService) UpdateAssignee(issueID string, assignee *User) (*Response, error) { +// Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.2/#api/2/issue-assign +// Caller must close resp.Body +func (s *IssueService) UpdateAssigneeWithContext(ctx context.Context, issueID string, assignee *User) (*Response, error) { apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/assignee", issueID) - req, err := s.client.NewRequest("PUT", apiEndPoint, assignee) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndPoint, assignee) if err != nil { return nil, err } @@ -1303,6 +1507,12 @@ func (s *IssueService) UpdateAssignee(issueID string, assignee *User) (*Response return resp, err } +// UpdateAssignee wraps UpdateAssigneeWithContext using the background context. +// Caller must close resp.Body +func (s *IssueService) UpdateAssignee(issueID string, assignee *User) (*Response, error) { + return s.UpdateAssigneeWithContext(context.Background(), issueID, assignee) +} + func (c ChangelogHistory) CreatedTime() (time.Time, error) { var t time.Time // Ignore null @@ -1313,12 +1523,12 @@ func (c ChangelogHistory) CreatedTime() (time.Time, error) { return t, err } -// GetRemoteLinks gets remote issue links on the issue. +// GetRemoteLinksWithContext gets remote issue links on the issue. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getRemoteIssueLinks -func (s *IssueService) GetRemoteLinks(id string) (*[]RemoteLink, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getRemoteIssueLinks +func (s *IssueService) GetRemoteLinksWithContext(ctx context.Context, id string) (*[]RemoteLink, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/remotelink", id) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -1330,3 +1540,58 @@ func (s *IssueService) GetRemoteLinks(id string) (*[]RemoteLink, *Response, erro } return result, resp, err } + +// GetRemoteLinks wraps GetRemoteLinksWithContext using the background context. +// Caller must close resp.Body +func (s *IssueService) GetRemoteLinks(id string) (*[]RemoteLink, *Response, error) { + return s.GetRemoteLinksWithContext(context.Background(), id) +} + +// AddRemoteLinkWithContext adds a remote link to issueID. +// +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-remotelink-post +func (s *IssueService) AddRemoteLinkWithContext(ctx context.Context, issueID string, remotelink *RemoteLink) (*RemoteLink, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/remotelink", issueID) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, remotelink) + if err != nil { + return nil, nil, err + } + + responseRemotelink := new(RemoteLink) + resp, err := s.client.Do(req, responseRemotelink) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return responseRemotelink, resp, nil +} + +// AddRemoteLink wraps AddRemoteLinkWithContext using the background context. +func (s *IssueService) AddRemoteLink(issueID string, remotelink *RemoteLink) (*RemoteLink, *Response, error) { + return s.AddRemoteLinkWithContext(context.Background(), issueID, remotelink) +} + +// UpdateRemoteLinkWithContext updates a remote issue link by linkID. +// +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-remote-links/#api-rest-api-2-issue-issueidorkey-remotelink-linkid-put +func (s *IssueService) UpdateRemoteLinkWithContext(ctx context.Context, issueID string, linkID int, remotelink *RemoteLink) (*Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/remotelink/%d", issueID, linkID) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, remotelink) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// UpdateRemoteLink wraps UpdateRemoteLinkWithContext using the background context. +func (s *IssueService) UpdateRemoteLink(issueID string, linkID int, remotelink *RemoteLink) (*Response, error) { + return s.UpdateRemoteLinkWithContext(context.Background(), issueID, linkID, remotelink) +} diff --git a/vendor/github.com/andygrunwald/go-jira/issuelinktype.go b/vendor/github.com/andygrunwald/go-jira/issuelinktype.go index 31eebc81..24a3ab0e 100644 --- a/vendor/github.com/andygrunwald/go-jira/issuelinktype.go +++ b/vendor/github.com/andygrunwald/go-jira/issuelinktype.go @@ -1,24 +1,25 @@ package jira import ( + "context" "encoding/json" "fmt" "io/ioutil" ) -// IssueLinkTypeService handles issue link types for the JIRA instance / API. +// IssueLinkTypeService handles issue link types for the Jira instance / API. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Issue-link-types +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Issue-link-types type IssueLinkTypeService struct { client *Client } -// GetList gets all of the issue link types from JIRA. +// GetListWithContext gets all of the issue link types from Jira. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-get -func (s *IssueLinkTypeService) GetList() ([]IssueLinkType, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-get +func (s *IssueLinkTypeService) GetListWithContext(ctx context.Context) ([]IssueLinkType, *Response, error) { apiEndpoint := "rest/api/2/issueLinkType" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -31,12 +32,17 @@ func (s *IssueLinkTypeService) GetList() ([]IssueLinkType, *Response, error) { return linkTypeList, resp, nil } -// Get gets info of a specific issue link type from JIRA. +// GetList wraps GetListWithContext using the background context. +func (s *IssueLinkTypeService) GetList() ([]IssueLinkType, *Response, error) { + return s.GetListWithContext(context.Background()) +} + +// GetWithContext gets info of a specific issue link type from Jira. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-get -func (s *IssueLinkTypeService) Get(ID string) (*IssueLinkType, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-get +func (s *IssueLinkTypeService) GetWithContext(ctx context.Context, ID string) (*IssueLinkType, *Response, error) { apiEndPoint := fmt.Sprintf("rest/api/2/issueLinkType/%s", ID) - req, err := s.client.NewRequest("GET", apiEndPoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) if err != nil { return nil, nil, err } @@ -49,12 +55,17 @@ func (s *IssueLinkTypeService) Get(ID string) (*IssueLinkType, *Response, error) return linkType, resp, nil } -// Create creates an issue link type in JIRA. +// Get wraps GetWithContext using the background context. +func (s *IssueLinkTypeService) Get(ID string) (*IssueLinkType, *Response, error) { + return s.GetWithContext(context.Background(), ID) +} + +// CreateWithContext creates an issue link type in Jira. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-post -func (s *IssueLinkTypeService) Create(linkType *IssueLinkType) (*IssueLinkType, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-post +func (s *IssueLinkTypeService) CreateWithContext(ctx context.Context, linkType *IssueLinkType) (*IssueLinkType, *Response, error) { apiEndpoint := "/rest/api/2/issueLinkType" - req, err := s.client.NewRequest("POST", apiEndpoint, linkType) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, linkType) if err != nil { return nil, nil, err } @@ -68,23 +79,29 @@ func (s *IssueLinkTypeService) Create(linkType *IssueLinkType) (*IssueLinkType, defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { - e := fmt.Errorf("Could not read the returned data") + e := fmt.Errorf("could not read the returned data") return nil, resp, NewJiraError(resp, e) } err = json.Unmarshal(data, responseLinkType) if err != nil { - e := fmt.Errorf("Could no unmarshal the data into struct") + e := fmt.Errorf("could no unmarshal the data into struct") return nil, resp, NewJiraError(resp, e) } return linkType, resp, nil } -// Update updates an issue link type. The issue is found by key. +// Create wraps CreateWithContext using the background context. +func (s *IssueLinkTypeService) Create(linkType *IssueLinkType) (*IssueLinkType, *Response, error) { + return s.CreateWithContext(context.Background(), linkType) +} + +// UpdateWithContext updates an issue link type. The issue is found by key. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-put -func (s *IssueLinkTypeService) Update(linkType *IssueLinkType) (*IssueLinkType, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-put +// Caller must close resp.Body +func (s *IssueLinkTypeService) UpdateWithContext(ctx context.Context, linkType *IssueLinkType) (*IssueLinkType, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issueLinkType/%s", linkType.ID) - req, err := s.client.NewRequest("PUT", apiEndpoint, linkType) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, linkType) if err != nil { return nil, nil, err } @@ -96,12 +113,19 @@ func (s *IssueLinkTypeService) Update(linkType *IssueLinkType) (*IssueLinkType, return &ret, resp, nil } -// Delete deletes an issue link type based on provided ID. +// Update wraps UpdateWithContext using the background context. +// Caller must close resp.Body +func (s *IssueLinkTypeService) Update(linkType *IssueLinkType) (*IssueLinkType, *Response, error) { + return s.UpdateWithContext(context.Background(), linkType) +} + +// DeleteWithContext deletes an issue link type based on provided ID. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-delete -func (s *IssueLinkTypeService) Delete(ID string) (*Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-delete +// Caller must close resp.Body +func (s *IssueLinkTypeService) DeleteWithContext(ctx context.Context, ID string) (*Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issueLinkType/%s", ID) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { return nil, err } @@ -109,3 +133,9 @@ func (s *IssueLinkTypeService) Delete(ID string) (*Response, error) { resp, err := s.client.Do(req, nil) return resp, err } + +// Delete wraps DeleteWithContext using the background context. +// Caller must close resp.Body +func (s *IssueLinkTypeService) Delete(ID string) (*Response, error) { + return s.DeleteWithContext(context.Background(), ID) +} diff --git a/vendor/github.com/andygrunwald/go-jira/jira.go b/vendor/github.com/andygrunwald/go-jira/jira.go index 34fa8cb4..25c52bae 100644 --- a/vendor/github.com/andygrunwald/go-jira/jira.go +++ b/vendor/github.com/andygrunwald/go-jira/jira.go @@ -2,6 +2,7 @@ package jira import ( "bytes" + "context" "crypto/sha256" "encoding/hex" "encoding/json" @@ -14,7 +15,7 @@ import ( "strings" "time" - "github.com/dgrijalva/jwt-go" + jwt "github.com/golang-jwt/jwt/v4" "github.com/google/go-querystring/query" "github.com/pkg/errors" ) @@ -25,7 +26,7 @@ type httpClient interface { Do(request *http.Request) (response *http.Response, err error) } -// A Client manages communication with the JIRA API. +// A Client manages communication with the Jira API. type Client struct { // HTTP client used to communicate with the API. client httpClient @@ -36,7 +37,7 @@ type Client struct { // Session storage if the user authenticates with a Session cookie session *Session - // Services used for talking to different parts of the JIRA API. + // Services used for talking to different parts of the Jira API. Authentication *AuthenticationService Issue *IssueService Project *ProjectService @@ -55,15 +56,19 @@ type Client struct { PermissionScheme *PermissionSchemeService Status *StatusService IssueLinkType *IssueLinkTypeService + Organization *OrganizationService + ServiceDesk *ServiceDeskService + Customer *CustomerService + Request *RequestService } -// NewClient returns a new JIRA API client. +// NewClient returns a new Jira API client. // If a nil httpClient is provided, http.DefaultClient will be used. // To use API methods which require authentication you can follow the preferred solution and // provide an http.Client that will perform the authentication for you with OAuth and HTTP Basic (such as that provided by the golang.org/x/oauth2 library). // As an alternative you can use Session Cookie based authentication provided by this package as well. // See https://docs.atlassian.com/jira/REST/latest/#authentication -// baseURL is the HTTP endpoint of your JIRA instance and should always be specified with a trailing slash. +// baseURL is the HTTP endpoint of your Jira instance and should always be specified with a trailing slash. func NewClient(httpClient httpClient, baseURL string) (*Client, error) { if httpClient == nil { httpClient = http.DefaultClient @@ -101,14 +106,18 @@ func NewClient(httpClient httpClient, baseURL string) (*Client, error) { c.PermissionScheme = &PermissionSchemeService{client: c} c.Status = &StatusService{client: c} c.IssueLinkType = &IssueLinkTypeService{client: c} + c.Organization = &OrganizationService{client: c} + c.ServiceDesk = &ServiceDeskService{client: c} + c.Customer = &CustomerService{client: c} + c.Request = &RequestService{client: c} return c, nil } -// NewRawRequest creates an API request. +// NewRawRequestWithContext creates an API request. // A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. // Allows using an optional native io.Reader for sourcing the request body. -func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Request, error) { +func (c *Client) NewRawRequestWithContext(ctx context.Context, method, urlStr string, body io.Reader) (*http.Request, error) { rel, err := url.Parse(urlStr) if err != nil { return nil, err @@ -118,7 +127,7 @@ func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Req u := c.baseURL.ResolveReference(rel) - req, err := http.NewRequest(method, u.String(), body) + req, err := newRequestWithContext(ctx, method, u.String(), body) if err != nil { return nil, err } @@ -143,10 +152,15 @@ func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Req return req, nil } -// NewRequest creates an API request. +// NewRawRequest wraps NewRawRequestWithContext using the background context. +func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Request, error) { + return c.NewRawRequestWithContext(context.Background(), method, urlStr, body) +} + +// NewRequestWithContext creates an API request. // A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. // If specified, the value pointed to by body is JSON encoded and included as the request body. -func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { +func (c *Client) NewRequestWithContext(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) { rel, err := url.Parse(urlStr) if err != nil { return nil, err @@ -165,7 +179,7 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ } } - req, err := http.NewRequest(method, u.String(), buf) + req, err := newRequestWithContext(ctx, method, u.String(), buf) if err != nil { return nil, err } @@ -190,6 +204,11 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ return req, nil } +// NewRequest wraps NewRequestWithContext using the background context. +func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { + return c.NewRequestWithContext(context.Background(), method, urlStr, body) +} + // addOptions adds the parameters in opt as URL query parameters to s. opt // must be a struct whose fields may contain "url" tags. func addOptions(s string, opt interface{}) (string, error) { @@ -212,10 +231,10 @@ func addOptions(s string, opt interface{}) (string, error) { return u.String(), nil } -// NewMultiPartRequest creates an API request including a multi-part file. +// NewMultiPartRequestWithContext creates an API request including a multi-part file. // A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. // If specified, the value pointed to by buf is a multipart form. -func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) { +func (c *Client) NewMultiPartRequestWithContext(ctx context.Context, method, urlStr string, buf *bytes.Buffer) (*http.Request, error) { rel, err := url.Parse(urlStr) if err != nil { return nil, err @@ -225,7 +244,7 @@ func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) ( u := c.baseURL.ResolveReference(rel) - req, err := http.NewRequest(method, u.String(), buf) + req, err := newRequestWithContext(ctx, method, u.String(), buf) if err != nil { return nil, err } @@ -251,6 +270,11 @@ func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) ( return req, nil } +// NewMultiPartRequest wraps NewMultiPartRequestWithContext using the background context. +func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) { + return c.NewMultiPartRequestWithContext(context.Background(), method, urlStr, buf) +} + // Do sends an API request and returns the API response. // The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred. func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { @@ -279,13 +303,13 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { // CheckResponse checks the API response for errors, and returns them if present. // A response is considered an error if it has a status code outside the 200 range. // The caller is responsible to analyze the response body. -// The body can contain JSON (if the error is intended) or xml (sometimes JIRA just failes). +// The body can contain JSON (if the error is intended) or xml (sometimes Jira just failes). func CheckResponse(r *http.Response) error { if c := r.StatusCode; 200 <= c && c <= 299 { return nil } - err := fmt.Errorf("Request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode) + err := fmt.Errorf("request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode) return err } @@ -295,7 +319,7 @@ func (c *Client) GetBaseURL() url.URL { return *c.baseURL } -// Response represents JIRA API response. It wraps http.Response returned from +// Response represents Jira API response. It wraps http.Response returned from // API and provides information about paging. type Response struct { *http.Response @@ -324,7 +348,6 @@ func (r *Response) populatePageValues(v interface{}) { r.MaxResults = value.MaxResults r.Total = value.Total } - return } // BasicAuthTransport is an http.RoundTripper that authenticates all requests @@ -363,13 +386,84 @@ func (t *BasicAuthTransport) transport() http.RoundTripper { return http.DefaultTransport } +// BearerAuthTransport is a http.RoundTripper that authenticates all requests +// using Jira's bearer (oauth 2.0 (3lo)) based authentication. +type BearerAuthTransport struct { + Token string + + // Transport is the underlying HTTP transport to use when making requests. + // It will default to http.DefaultTransport if nil. + Transport http.RoundTripper +} + +// RoundTrip implements the RoundTripper interface. We just add the +// bearer token and return the RoundTripper for this transport type. +func (t *BearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req2 := cloneRequest(req) // per RoundTripper contract + + req2.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.Token)) + return t.transport().RoundTrip(req2) +} + +// Client returns an *http.Client that makes requests that are authenticated +// using HTTP Basic Authentication. This is a nice little bit of sugar +// so we can just get the client instead of creating the client in the calling code. +// If it's necessary to send more information on client init, the calling code can +// always skip this and set the transport itself. +func (t *BearerAuthTransport) Client() *http.Client { + return &http.Client{Transport: t} +} + +func (t *BearerAuthTransport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} + +// PATAuthTransport is an http.RoundTripper that authenticates all requests +// using the Personal Access Token specified. +// See here for more info: https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html +type PATAuthTransport struct { + // Token is the key that was provided by Jira when creating the Personal Access Token. + Token string + + // Transport is the underlying HTTP transport to use when making requests. + // It will default to http.DefaultTransport if nil. + Transport http.RoundTripper +} + +// RoundTrip implements the RoundTripper interface. We just add the +// basic auth and return the RoundTripper for this transport type. +func (t *PATAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req2 := cloneRequest(req) // per RoundTripper contract + req2.Header.Set("Authorization", "Bearer "+t.Token) + return t.transport().RoundTrip(req2) +} + +// Client returns an *http.Client that makes requests that are authenticated +// using HTTP Basic Authentication. This is a nice little bit of sugar +// so we can just get the client instead of creating the client in the calling code. +// If it's necessary to send more information on client init, the calling code can +// always skip this and set the transport itself. +func (t *PATAuthTransport) Client() *http.Client { + return &http.Client{Transport: t} +} + +func (t *PATAuthTransport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} + // CookieAuthTransport is an http.RoundTripper that authenticates all requests // using Jira's cookie-based authentication. // // Note that it is generally preferable to use HTTP BASIC authentication with the REST API. -// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user). +// However, this resource may be used to mimic the behaviour of Jira's log-in page (e.g. to display log-in errors to a user). // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session type CookieAuthTransport struct { Username string Password string @@ -425,6 +519,7 @@ func (t *CookieAuthTransport) setSessionObject() error { if err != nil { return err } + defer resp.Body.Close() t.SessionObject = resp.Cookies() return nil @@ -464,7 +559,7 @@ func (t *CookieAuthTransport) transport() http.RoundTripper { // // NOTE: this form of auth should be used by add-ons installed from the Atlassian marketplace. // -// JIRA docs: https://developer.atlassian.com/cloud/jira/platform/understanding-jwt +// Jira docs: https://developer.atlassian.com/cloud/jira/platform/understanding-jwt // Examples in other languages: // https://bitbucket.org/atlassian/atlassian-jwt-ruby/src/d44a8e7a4649e4f23edaa784402655fda7c816ea/lib/atlassian/jwt.rb // https://bitbucket.org/atlassian/atlassian-jwt-py/src/master/atlassian_jwt/url_utils.py diff --git a/vendor/github.com/andygrunwald/go-jira/metaissue.go b/vendor/github.com/andygrunwald/go-jira/metaissue.go index 19813786..560a2f05 100644 --- a/vendor/github.com/andygrunwald/go-jira/metaissue.go +++ b/vendor/github.com/andygrunwald/go-jira/metaissue.go @@ -1,6 +1,7 @@ package jira import ( + "context" "fmt" "strings" @@ -14,6 +15,11 @@ type CreateMetaInfo struct { Projects []*MetaProject `json:"projects,omitempty"` } +// EditMetaInfo contains information about fields and their attributed to edit a ticket. +type EditMetaInfo struct { + Fields tcontainer.MarshalMap `json:"fields,omitempty"` +} + // MetaProject is the meta information about a project returned from createmeta api type MetaProject struct { Expand string `json:"expand,omitempty"` @@ -42,16 +48,21 @@ type MetaIssueType struct { Fields tcontainer.MarshalMap `json:"fields,omitempty"` } -// GetCreateMeta makes the api call to get the meta information required to create a ticket -func (s *IssueService) GetCreateMeta(projectkeys string) (*CreateMetaInfo, *Response, error) { - return s.GetCreateMetaWithOptions(&GetQueryOptions{ProjectKeys: projectkeys, Expand: "projects.issuetypes.fields"}) +// GetCreateMetaWithContext makes the api call to get the meta information required to create a ticket +func (s *IssueService) GetCreateMetaWithContext(ctx context.Context, projectkeys string) (*CreateMetaInfo, *Response, error) { + return s.GetCreateMetaWithOptionsWithContext(ctx, &GetQueryOptions{ProjectKeys: projectkeys, Expand: "projects.issuetypes.fields"}) } -// GetCreateMetaWithOptions makes the api call to get the meta information without requiring to have a projectKey -func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*CreateMetaInfo, *Response, error) { +// GetCreateMeta wraps GetCreateMetaWithContext using the background context. +func (s *IssueService) GetCreateMeta(projectkeys string) (*CreateMetaInfo, *Response, error) { + return s.GetCreateMetaWithContext(context.Background(), projectkeys) +} + +// GetCreateMetaWithOptionsWithContext makes the api call to get the meta information without requiring to have a projectKey +func (s *IssueService) GetCreateMetaWithOptionsWithContext(ctx context.Context, options *GetQueryOptions) (*CreateMetaInfo, *Response, error) { apiEndpoint := "rest/api/2/issue/createmeta" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -73,11 +84,40 @@ func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*Crea return meta, resp, nil } +// GetCreateMetaWithOptions wraps GetCreateMetaWithOptionsWithContext using the background context. +func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*CreateMetaInfo, *Response, error) { + return s.GetCreateMetaWithOptionsWithContext(context.Background(), options) +} + +// GetEditMetaWithContext makes the api call to get the edit meta information for an issue +func (s *IssueService) GetEditMetaWithContext(ctx context.Context, issue *Issue) (*EditMetaInfo, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/issue/%s/editmeta", issue.Key) + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + meta := new(EditMetaInfo) + resp, err := s.client.Do(req, meta) + + if err != nil { + return nil, resp, err + } + + return meta, resp, nil +} + +// GetEditMeta wraps GetEditMetaWithContext using the background context. +func (s *IssueService) GetEditMeta(issue *Issue) (*EditMetaInfo, *Response, error) { + return s.GetEditMetaWithContext(context.Background(), issue) +} + // GetProjectWithName returns a project with "name" from the meta information received. If not found, this returns nil. // The comparison of the name is case insensitive. func (m *CreateMetaInfo) GetProjectWithName(name string) *MetaProject { for _, m := range m.Projects { - if strings.ToLower(m.Name) == strings.ToLower(name) { + if strings.EqualFold(m.Name, name) { return m } } @@ -88,7 +128,7 @@ func (m *CreateMetaInfo) GetProjectWithName(name string) *MetaProject { // The comparison of the name is case insensitive. func (m *CreateMetaInfo) GetProjectWithKey(key string) *MetaProject { for _, m := range m.Projects { - if strings.ToLower(m.Key) == strings.ToLower(key) { + if strings.EqualFold(m.Key, key) { return m } } @@ -99,7 +139,7 @@ func (m *CreateMetaInfo) GetProjectWithKey(key string) *MetaProject { // The comparison of the name is case insensitive func (p *MetaProject) GetIssueTypeWithName(name string) *MetaIssueType { for _, m := range p.IssueTypes { - if strings.ToLower(m.Name) == strings.ToLower(name) { + if strings.EqualFold(m.Name, name) { return m } } @@ -175,7 +215,7 @@ func (t *MetaIssueType) CheckCompleteAndAvailable(config map[string]string) (boo for name := range mandatory { requiredFields = append(requiredFields, name) } - return false, fmt.Errorf("Required field not found in provided jira.fields. Required are: %#v", requiredFields) + return false, fmt.Errorf("required field not found in provided jira.fields. Required are: %#v", requiredFields) } } @@ -186,7 +226,7 @@ func (t *MetaIssueType) CheckCompleteAndAvailable(config map[string]string) (boo for name := range all { availableFields = append(availableFields, name) } - return false, fmt.Errorf("Fields in jira.fields are not available in jira. Available are: %#v", availableFields) + return false, fmt.Errorf("fields in jira.fields are not available in jira. Available are: %#v", availableFields) } } diff --git a/vendor/github.com/andygrunwald/go-jira/organization.go b/vendor/github.com/andygrunwald/go-jira/organization.go new file mode 100644 index 00000000..65f222b3 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/organization.go @@ -0,0 +1,397 @@ +package jira + +import ( + "context" + "fmt" +) + +// OrganizationService handles Organizations for the Jira instance / API. +// +// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/ +type OrganizationService struct { + client *Client +} + +// OrganizationCreationDTO is DTO for creat organization API +type OrganizationCreationDTO struct { + Name string `json:"name,omitempty" structs:"name,omitempty"` +} + +// SelfLink Stores REST API URL to the organization. +type SelfLink struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` +} + +// Organization contains Organization data +type Organization struct { + ID string `json:"id,omitempty" structs:"id,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + Links *SelfLink `json:"_links,omitempty" structs:"_links,omitempty"` +} + +// OrganizationUsersDTO contains organization user ids +type OrganizationUsersDTO struct { + AccountIds []string `json:"accountIds,omitempty" structs:"accountIds,omitempty"` +} + +// PagedDTO is response of a paged list +type PagedDTO struct { + Size int `json:"size,omitempty" structs:"size,omitempty"` + Start int `json:"start,omitempty" structs:"start,omitempty"` + Limit int `limit:"size,omitempty" structs:"limit,omitempty"` + IsLastPage bool `json:"isLastPage,omitempty" structs:"isLastPage,omitempty"` + Values []interface{} `values:"isLastPage,omitempty" structs:"values,omitempty"` + Expands []string `json:"_expands,omitempty" structs:"_expands,omitempty"` +} + +// PropertyKey contains Property key details. +type PropertyKey struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` + Key string `json:"key,omitempty" structs:"key,omitempty"` +} + +// PropertyKeys contains an array of PropertyKey +type PropertyKeys struct { + Keys []PropertyKey `json:"keys,omitempty" structs:"keys,omitempty"` +} + +// GetAllOrganizationsWithContext returns a list of organizations in +// the Jira Service Management instance. +// Use this method when you want to present a list +// of organizations or want to locate an organization +// by name. +// +// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-group-organization +func (s *OrganizationService) GetAllOrganizationsWithContext(ctx context.Context, start int, limit int, accountID string) (*PagedDTO, *Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization?start=%d&limit=%d", start, limit) + if accountID != "" { + apiEndPoint += fmt.Sprintf("&accountId=%s", accountID) + } + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + v := new(PagedDTO) + resp, err := s.client.Do(req, v) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return v, resp, nil +} + +// GetAllOrganizations wraps GetAllOrganizationsWithContext using the background context. +func (s *OrganizationService) GetAllOrganizations(start int, limit int, accountID string) (*PagedDTO, *Response, error) { + return s.GetAllOrganizationsWithContext(context.Background(), start, limit, accountID) +} + +// CreateOrganizationWithContext creates an organization by +// passing the name of the organization. +// +// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-post +func (s *OrganizationService) CreateOrganizationWithContext(ctx context.Context, name string) (*Organization, *Response, error) { + apiEndPoint := "rest/servicedeskapi/organization" + + organization := OrganizationCreationDTO{ + Name: name, + } + + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, organization) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + o := new(Organization) + resp, err := s.client.Do(req, &o) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return o, resp, nil +} + +// CreateOrganization wraps CreateOrganizationWithContext using the background context. +func (s *OrganizationService) CreateOrganization(name string) (*Organization, *Response, error) { + return s.CreateOrganizationWithContext(context.Background(), name) +} + +// GetOrganizationWithContext returns details of an +// organization. Use this method to get organization +// details whenever your application component is +// passed an organization ID but needs to display +// other organization details. +// +// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-get +func (s *OrganizationService) GetOrganizationWithContext(ctx context.Context, organizationID int) (*Organization, *Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d", organizationID) + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + o := new(Organization) + resp, err := s.client.Do(req, &o) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return o, resp, nil +} + +// GetOrganization wraps GetOrganizationWithContext using the background context. +func (s *OrganizationService) GetOrganization(organizationID int) (*Organization, *Response, error) { + return s.GetOrganizationWithContext(context.Background(), organizationID) +} + +// DeleteOrganizationWithContext deletes an organization. Note that +// the organization is deleted regardless +// of other associations it may have. +// For example, associations with service desks. +// +// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-delete +// Caller must close resp.Body +func (s *OrganizationService) DeleteOrganizationWithContext(ctx context.Context, organizationID int) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d", organizationID) + + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, nil) + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// DeleteOrganization wraps DeleteOrganizationWithContext using the background context. +// Caller must close resp.Body +func (s *OrganizationService) DeleteOrganization(organizationID int) (*Response, error) { + return s.DeleteOrganizationWithContext(context.Background(), organizationID) +} + +// GetPropertiesKeysWithContext returns the keys of +// all properties for an organization. Use this resource +// when you need to find out what additional properties +// items have been added to an organization. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-get +func (s *OrganizationService) GetPropertiesKeysWithContext(ctx context.Context, organizationID int) (*PropertyKeys, *Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property", organizationID) + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + pk := new(PropertyKeys) + resp, err := s.client.Do(req, &pk) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return pk, resp, nil +} + +// GetPropertiesKeys wraps GetPropertiesKeysWithContext using the background context. +func (s *OrganizationService) GetPropertiesKeys(organizationID int) (*PropertyKeys, *Response, error) { + return s.GetPropertiesKeysWithContext(context.Background(), organizationID) +} + +// GetPropertyWithContext returns the value of a property +// from an organization. Use this method to obtain the JSON +// content for an organization's property. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-propertykey-get +func (s *OrganizationService) GetPropertyWithContext(ctx context.Context, organizationID int, propertyKey string) (*EntityProperty, *Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property/%s", organizationID, propertyKey) + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + ep := new(EntityProperty) + resp, err := s.client.Do(req, &ep) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return ep, resp, nil +} + +// GetProperty wraps GetPropertyWithContext using the background context. +func (s *OrganizationService) GetProperty(organizationID int, propertyKey string) (*EntityProperty, *Response, error) { + return s.GetPropertyWithContext(context.Background(), organizationID, propertyKey) +} + +// SetPropertyWithContext sets the value of a +// property for an organization. Use this +// resource to store custom data against an organization. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-propertykey-put +// Caller must close resp.Body +func (s *OrganizationService) SetPropertyWithContext(ctx context.Context, organizationID int, propertyKey string) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property/%s", organizationID, propertyKey) + + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// SetProperty wraps SetPropertyWithContext using the background context. +// Caller must close resp.Body +func (s *OrganizationService) SetProperty(organizationID int, propertyKey string) (*Response, error) { + return s.SetPropertyWithContext(context.Background(), organizationID, propertyKey) +} + +// DeletePropertyWithContext removes a property from an organization. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-propertykey-delete +// Caller must close resp.Body +func (s *OrganizationService) DeletePropertyWithContext(ctx context.Context, organizationID int, propertyKey string) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property/%s", organizationID, propertyKey) + + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// DeleteProperty wraps DeletePropertyWithContext using the background context. +// Caller must close resp.Body +func (s *OrganizationService) DeleteProperty(organizationID int, propertyKey string) (*Response, error) { + return s.DeletePropertyWithContext(context.Background(), organizationID, propertyKey) +} + +// GetUsersWithContext returns all the users +// associated with an organization. Use this +// method where you want to provide a list of +// users for an organization or determine if +// a user is associated with an organization. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-user-get +func (s *OrganizationService) GetUsersWithContext(ctx context.Context, organizationID int, start int, limit int) (*PagedDTO, *Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/user?start=%d&limit=%d", organizationID, start, limit) + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + users := new(PagedDTO) + resp, err := s.client.Do(req, &users) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return users, resp, nil +} + +// GetUsers wraps GetUsersWithContext using the background context. +func (s *OrganizationService) GetUsers(organizationID int, start int, limit int) (*PagedDTO, *Response, error) { + return s.GetUsersWithContext(context.Background(), organizationID, start, limit) +} + +// AddUsersWithContext adds users to an organization. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-user-post +// Caller must close resp.Body +func (s *OrganizationService) AddUsersWithContext(ctx context.Context, organizationID int, users OrganizationUsersDTO) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/user", organizationID) + + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, users) + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// AddUsers wraps AddUsersWithContext using the background context. +// Caller must close resp.Body +func (s *OrganizationService) AddUsers(organizationID int, users OrganizationUsersDTO) (*Response, error) { + return s.AddUsersWithContext(context.Background(), organizationID, users) +} + +// RemoveUsersWithContext removes users from an organization. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-user-delete +// Caller must close resp.Body +func (s *OrganizationService) RemoveUsersWithContext(ctx context.Context, organizationID int, users OrganizationUsersDTO) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/user", organizationID) + + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// RemoveUsers wraps RemoveUsersWithContext using the background context. +// Caller must close resp.Body +func (s *OrganizationService) RemoveUsers(organizationID int, users OrganizationUsersDTO) (*Response, error) { + return s.RemoveUsersWithContext(context.Background(), organizationID, users) +} diff --git a/vendor/github.com/andygrunwald/go-jira/permissionscheme.go b/vendor/github.com/andygrunwald/go-jira/permissionscheme.go index 6a48d65f..7af5a8bf 100644 --- a/vendor/github.com/andygrunwald/go-jira/permissionscheme.go +++ b/vendor/github.com/andygrunwald/go-jira/permissionscheme.go @@ -1,10 +1,13 @@ package jira -import "fmt" +import ( + "context" + "fmt" +) -// PermissionSchemeService handles permissionschemes for the JIRA instance / API. +// PermissionSchemeService handles permissionschemes for the Jira instance / API. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Permissionscheme +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Permissionscheme type PermissionSchemeService struct { client *Client } @@ -25,12 +28,12 @@ type Holder struct { Expand string `json:"expand" structs:"expand"` } -// GetList returns a list of all permission schemes +// GetListWithContext returns a list of all permission schemes // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-get -func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-get +func (s *PermissionSchemeService) GetListWithContext(ctx context.Context) (*PermissionSchemes, *Response, error) { apiEndpoint := "/rest/api/3/permissionscheme" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -45,12 +48,17 @@ func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, erro return pss, resp, nil } -// Get returns a full representation of the permission scheme for the schemeID +// GetList wraps GetListWithContext using the background context. +func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, error) { + return s.GetListWithContext(context.Background()) +} + +// GetWithContext returns a full representation of the permission scheme for the schemeID // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-schemeId-get -func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-schemeId-get +func (s *PermissionSchemeService) GetWithContext(ctx context.Context, schemeID int) (*PermissionScheme, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/3/permissionscheme/%d", schemeID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -62,8 +70,13 @@ func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Respons return nil, resp, jerr } if ps.Self == "" { - return nil, resp, fmt.Errorf("No permissionscheme with ID %d found", schemeID) + return nil, resp, fmt.Errorf("no permissionscheme with ID %d found", schemeID) } return ps, resp, nil } + +// Get wraps GetWithContext using the background context. +func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Response, error) { + return s.GetWithContext(context.Background(), schemeID) +} diff --git a/vendor/github.com/andygrunwald/go-jira/priority.go b/vendor/github.com/andygrunwald/go-jira/priority.go index 481f9592..a7b12a41 100644 --- a/vendor/github.com/andygrunwald/go-jira/priority.go +++ b/vendor/github.com/andygrunwald/go-jira/priority.go @@ -1,13 +1,15 @@ package jira -// PriorityService handles priorities for the JIRA instance / API. +import "context" + +// PriorityService handles priorities for the Jira instance / API. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Priority +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Priority type PriorityService struct { client *Client } -// Priority represents a priority of a JIRA issue. +// Priority represents a priority of a Jira issue. // Typical types are "Normal", "Moderate", "Urgent", ... type Priority struct { Self string `json:"self,omitempty" structs:"self,omitempty"` @@ -18,12 +20,12 @@ type Priority struct { Description string `json:"description,omitempty" structs:"description,omitempty"` } -// GetList gets all priorities from JIRA +// GetListWithContext gets all priorities from Jira // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-priority-get -func (s *PriorityService) GetList() ([]Priority, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-priority-get +func (s *PriorityService) GetListWithContext(ctx context.Context) ([]Priority, *Response, error) { apiEndpoint := "rest/api/2/priority" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -35,3 +37,8 @@ func (s *PriorityService) GetList() ([]Priority, *Response, error) { } return priorityList, resp, nil } + +// GetList wraps GetListWithContext using the background context. +func (s *PriorityService) GetList() ([]Priority, *Response, error) { + return s.GetListWithContext(context.Background()) +} diff --git a/vendor/github.com/andygrunwald/go-jira/project.go b/vendor/github.com/andygrunwald/go-jira/project.go index 24184224..f1000c81 100644 --- a/vendor/github.com/andygrunwald/go-jira/project.go +++ b/vendor/github.com/andygrunwald/go-jira/project.go @@ -1,14 +1,15 @@ package jira import ( + "context" "fmt" "github.com/google/go-querystring/query" ) -// ProjectService handles projects for the JIRA instance / API. +// ProjectService handles projects for the Jira instance / API. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project type ProjectService struct { client *Client } @@ -34,7 +35,7 @@ type ProjectCategory struct { Description string `json:"description" structs:"description,omitempty"` } -// Project represents a JIRA Project. +// Project represents a Jira Project. type Project struct { Expand string `json:"expand,omitempty" structs:"expand,omitempty"` Self string `json:"self,omitempty" structs:"self,omitempty"` @@ -80,20 +81,25 @@ type PermissionScheme struct { Permissions []Permission `json:"permissions" structs:"permissions,omitempty"` } -// GetList gets all projects form JIRA +// GetListWithContext gets all projects form Jira // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects -func (s *ProjectService) GetList() (*ProjectList, *Response, error) { - return s.ListWithOptions(&GetQueryOptions{}) +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects +func (s *ProjectService) GetListWithContext(ctx context.Context) (*ProjectList, *Response, error) { + return s.ListWithOptionsWithContext(ctx, &GetQueryOptions{}) } -// ListWithOptions gets all projects form JIRA with optional query params, like &GetQueryOptions{Expand: "issueTypes"} to get +// GetList wraps GetListWithContext using the background context. +func (s *ProjectService) GetList() (*ProjectList, *Response, error) { + return s.GetListWithContext(context.Background()) +} + +// ListWithOptionsWithContext gets all projects form Jira with optional query params, like &GetQueryOptions{Expand: "issueTypes"} to get // a list of all projects and their supported issuetypes // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects -func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects +func (s *ProjectService) ListWithOptionsWithContext(ctx context.Context, options *GetQueryOptions) (*ProjectList, *Response, error) { apiEndpoint := "rest/api/2/project" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -116,14 +122,19 @@ func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList return projectList, resp, nil } -// Get returns a full representation of the project for the given issue key. -// JIRA will attempt to identify the project by the projectIdOrKey path parameter. +// ListWithOptions wraps ListWithOptionsWithContext using the background context. +func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList, *Response, error) { + return s.ListWithOptionsWithContext(context.Background(), options) +} + +// GetWithContext returns a full representation of the project for the given issue key. +// Jira will attempt to identify the project by the projectIdOrKey path parameter. // This can be an project id, or an project key. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject -func (s *ProjectService) Get(projectID string) (*Project, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject +func (s *ProjectService) GetWithContext(ctx context.Context, projectID string) (*Project, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/project/%s", projectID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -138,14 +149,19 @@ func (s *ProjectService) Get(projectID string) (*Project, *Response, error) { return project, resp, nil } -// GetPermissionScheme returns a full representation of the permission scheme for the project -// JIRA will attempt to identify the project by the projectIdOrKey path parameter. +// Get wraps GetWithContext using the background context. +func (s *ProjectService) Get(projectID string) (*Project, *Response, error) { + return s.GetWithContext(context.Background(), projectID) +} + +// GetPermissionSchemeWithContext returns a full representation of the permission scheme for the project +// Jira will attempt to identify the project by the projectIdOrKey path parameter. // This can be an project id, or an project key. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject -func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionScheme, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject +func (s *ProjectService) GetPermissionSchemeWithContext(ctx context.Context, projectID string) (*PermissionScheme, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/project/%s/permissionscheme", projectID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -159,3 +175,8 @@ func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionSchem return ps, resp, nil } + +// GetPermissionScheme wraps GetPermissionSchemeWithContext using the background context. +func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionScheme, *Response, error) { + return s.GetPermissionSchemeWithContext(context.Background(), projectID) +} diff --git a/vendor/github.com/andygrunwald/go-jira/request.go b/vendor/github.com/andygrunwald/go-jira/request.go new file mode 100644 index 00000000..a933a57a --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/request.go @@ -0,0 +1,123 @@ +package jira + +import ( + "context" + "fmt" +) + +// RequestService handles ServiceDesk customer requests for the Jira instance / API. +type RequestService struct { + client *Client +} + +// Request represents a ServiceDesk customer request. +type Request struct { + IssueID string `json:"issueId,omitempty" structs:"issueId,omitempty"` + IssueKey string `json:"issueKey,omitempty" structs:"issueKey,omitempty"` + TypeID string `json:"requestTypeId,omitempty" structs:"requestTypeId,omitempty"` + ServiceDeskID string `json:"serviceDeskId,omitempty" structs:"serviceDeskId,omitempty"` + Reporter *Customer `json:"reporter,omitempty" structs:"reporter,omitempty"` + FieldValues []RequestFieldValue `json:"requestFieldValues,omitempty" structs:"requestFieldValues,omitempty"` + Status *RequestStatus `json:"currentStatus,omitempty" structs:"currentStatus,omitempty"` + Links *SelfLink `json:"_links,omitempty" structs:"_links,omitempty"` + Expands []string `json:"_expands,omitempty" structs:"_expands,omitempty"` +} + +// RequestFieldValue is a request field. +type RequestFieldValue struct { + FieldID string `json:"fieldId,omitempty" structs:"fieldId,omitempty"` + Label string `json:"label,omitempty" structs:"label,omitempty"` + Value string `json:"value,omitempty" structs:"value,omitempty"` +} + +// RequestDate is the date format used in requests. +type RequestDate struct { + ISO8601 string `json:"iso8601,omitempty" structs:"iso8601,omitempty"` + Jira string `json:"jira,omitempty" structs:"jira,omitempty"` + Friendly string `json:"friendly,omitempty" structs:"friendly,omitempty"` + Epoch int64 `json:"epoch,omitempty" structs:"epoch,omitempty"` +} + +// RequestStatus is the status for a request. +type RequestStatus struct { + Status string + Category string + Date RequestDate +} + +// RequestComment is a comment for a request. +type RequestComment struct { + ID string `json:"id,omitempty" structs:"id,omitempty"` + Body string `json:"body,omitempty" structs:"body,omitempty"` + Public bool `json:"public" structs:"public"` + Author *Customer `json:"author,omitempty" structs:"author,omitempty"` + Created *RequestDate `json:"created,omitempty" structs:"created,omitempty"` + Links *SelfLink `json:"_links,omitempty" structs:"_links,omitempty"` + Expands []string `json:"_expands,omitempty" structs:"_expands,omitempty"` +} + +// CreateWithContext creates a new request. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-post +func (r *RequestService) CreateWithContext(ctx context.Context, requester string, participants []string, request *Request) (*Request, *Response, error) { + apiEndpoint := "rest/servicedeskapi/request" + + payload := struct { + *Request + FieldValues map[string]string `json:"requestFieldValues,omitempty"` + Requester string `json:"raiseOnBehalfOf,omitempty"` + Participants []string `json:"requestParticipants,omitempty"` + }{ + Request: request, + FieldValues: make(map[string]string), + Requester: requester, + Participants: participants, + } + + for _, field := range request.FieldValues { + payload.FieldValues[field.FieldID] = field.Value + } + + req, err := r.client.NewRequestWithContext(ctx, "POST", apiEndpoint, payload) + if err != nil { + return nil, nil, err + } + + responseRequest := new(Request) + resp, err := r.client.Do(req, responseRequest) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + + return responseRequest, resp, nil +} + +// Create wraps CreateWithContext using the background context. +func (r *RequestService) Create(requester string, participants []string, request *Request) (*Request, *Response, error) { + return r.CreateWithContext(context.Background(), requester, participants, request) +} + +// CreateCommentWithContext creates a comment on a request. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-issueidorkey-comment-post +func (r *RequestService) CreateCommentWithContext(ctx context.Context, issueIDOrKey string, comment *RequestComment) (*RequestComment, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/servicedeskapi/request/%v/comment", issueIDOrKey) + + req, err := r.client.NewRequestWithContext(ctx, "POST", apiEndpoint, comment) + if err != nil { + return nil, nil, err + } + + responseComment := new(RequestComment) + resp, err := r.client.Do(req, responseComment) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + + return responseComment, resp, nil +} + +// CreateComment wraps CreateCommentWithContext using the background context. +func (r *RequestService) CreateComment(issueIDOrKey string, comment *RequestComment) (*RequestComment, *Response, error) { + return r.CreateCommentWithContext(context.Background(), issueIDOrKey, comment) +} diff --git a/vendor/github.com/andygrunwald/go-jira/request_context.go b/vendor/github.com/andygrunwald/go-jira/request_context.go new file mode 100644 index 00000000..9f3e2647 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/request_context.go @@ -0,0 +1,24 @@ +//go:build go1.13 +// +build go1.13 + +// This file provides glue to use Context in `http.Request` with +// Go version 1.13 and higher. + +// The function `http.NewRequestWithContext` has been added in Go 1.13. +// Before the release 1.13, to use Context we need creat `http.Request` +// then use the method `WithContext` to create a new `http.Request` +// with Context from the existing `http.Request`. +// +// Doc: https://golang.org/doc/go1.13#net/http + +package jira + +import ( + "context" + "io" + "net/http" +) + +func newRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) { + return http.NewRequestWithContext(ctx, method, url, body) +} diff --git a/vendor/github.com/andygrunwald/go-jira/request_legacy.go b/vendor/github.com/andygrunwald/go-jira/request_legacy.go new file mode 100644 index 00000000..93eb65e8 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/request_legacy.go @@ -0,0 +1,29 @@ +//go:build !go1.13 +// +build !go1.13 + +// This file provides glue to use Context in `http.Request` with +// Go version before 1.13. + +// The function `http.NewRequestWithContext` has been added in Go 1.13. +// Before the release 1.13, to use Context we need creat `http.Request` +// then use the method `WithContext` to create a new `http.Request` +// with Context from the existing `http.Request`. +// +// Doc: https://golang.org/doc/go1.13#net/http + +package jira + +import ( + "context" + "io" + "net/http" +) + +func newRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) { + r, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + + return r.WithContext(ctx), nil +} diff --git a/vendor/github.com/andygrunwald/go-jira/resolution.go b/vendor/github.com/andygrunwald/go-jira/resolution.go index 36a651fb..e23d5650 100644 --- a/vendor/github.com/andygrunwald/go-jira/resolution.go +++ b/vendor/github.com/andygrunwald/go-jira/resolution.go @@ -1,13 +1,15 @@ package jira -// ResolutionService handles resolutions for the JIRA instance / API. +import "context" + +// ResolutionService handles resolutions for the Jira instance / API. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Resolution +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Resolution type ResolutionService struct { client *Client } -// Resolution represents a resolution of a JIRA issue. +// Resolution represents a resolution of a Jira issue. // Typical types are "Fixed", "Suspended", "Won't Fix", ... type Resolution struct { Self string `json:"self" structs:"self"` @@ -16,12 +18,12 @@ type Resolution struct { Name string `json:"name" structs:"name"` } -// GetList gets all resolutions from JIRA +// GetListWithContext gets all resolutions from Jira // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-resolution-get -func (s *ResolutionService) GetList() ([]Resolution, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-resolution-get +func (s *ResolutionService) GetListWithContext(ctx context.Context) ([]Resolution, *Response, error) { apiEndpoint := "rest/api/2/resolution" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -33,3 +35,8 @@ func (s *ResolutionService) GetList() ([]Resolution, *Response, error) { } return resolutionList, resp, nil } + +// GetList wraps GetListWithContext using the background context. +func (s *ResolutionService) GetList() ([]Resolution, *Response, error) { + return s.GetListWithContext(context.Background()) +} diff --git a/vendor/github.com/andygrunwald/go-jira/role.go b/vendor/github.com/andygrunwald/go-jira/role.go index f2535e4a..66d223ff 100644 --- a/vendor/github.com/andygrunwald/go-jira/role.go +++ b/vendor/github.com/andygrunwald/go-jira/role.go @@ -1,17 +1,18 @@ package jira import ( + "context" "fmt" ) -// RoleService handles roles for the JIRA instance / API. +// RoleService handles roles for the Jira instance / API. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Role +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Role type RoleService struct { client *Client } -// Role represents a JIRA product role +// Role represents a Jira product role type Role struct { Self string `json:"self" structs:"self"` Name string `json:"name" structs:"name"` @@ -20,7 +21,7 @@ type Role struct { Actors []*Actor `json:"actors" structs:"actors"` } -// Actor represents a JIRA actor +// Actor represents a Jira actor type Actor struct { ID int `json:"id" structs:"id"` DisplayName string `json:"displayName" structs:"displayName"` @@ -35,12 +36,12 @@ type ActorUser struct { AccountID string `json:"accountId" structs:"accountId"` } -// GetList returns a list of all available project roles +// GetListWithContext returns a list of all available project roles // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-get -func (s *RoleService) GetList() (*[]Role, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-get +func (s *RoleService) GetListWithContext(ctx context.Context) (*[]Role, *Response, error) { apiEndpoint := "rest/api/3/role" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -53,12 +54,17 @@ func (s *RoleService) GetList() (*[]Role, *Response, error) { return roles, resp, err } -// Get retreives a single Role from Jira +// GetList wraps GetListWithContext using the background context. +func (s *RoleService) GetList() (*[]Role, *Response, error) { + return s.GetListWithContext(context.Background()) +} + +// GetWithContext retreives a single Role from Jira // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-id-get -func (s *RoleService) Get(roleID int) (*Role, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-id-get +func (s *RoleService) GetWithContext(ctx context.Context, roleID int) (*Role, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/3/role/%d", roleID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -69,8 +75,13 @@ func (s *RoleService) Get(roleID int) (*Role, *Response, error) { return nil, resp, jerr } if role.Self == "" { - return nil, resp, fmt.Errorf("No role with ID %d found", roleID) + return nil, resp, fmt.Errorf("no role with ID %d found", roleID) } return role, resp, err } + +// Get wraps GetWithContext using the background context. +func (s *RoleService) Get(roleID int) (*Role, *Response, error) { + return s.GetWithContext(context.Background(), roleID) +} diff --git a/vendor/github.com/andygrunwald/go-jira/servicedesk.go b/vendor/github.com/andygrunwald/go-jira/servicedesk.go new file mode 100644 index 00000000..7fbffc0b --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/servicedesk.go @@ -0,0 +1,227 @@ +package jira + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + + "github.com/google/go-querystring/query" +) + +// ServiceDeskService handles ServiceDesk for the Jira instance / API. +type ServiceDeskService struct { + client *Client +} + +// ServiceDeskOrganizationDTO is a DTO for ServiceDesk organizations +type ServiceDeskOrganizationDTO struct { + OrganizationID int `json:"organizationId,omitempty" structs:"organizationId,omitempty"` +} + +// GetOrganizationsWithContext returns a list of +// all organizations associated with a service desk. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-get +func (s *ServiceDeskService) GetOrganizationsWithContext(ctx context.Context, serviceDeskID interface{}, start int, limit int, accountID string) (*PagedDTO, *Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/organization?start=%d&limit=%d", serviceDeskID, start, limit) + if accountID != "" { + apiEndPoint += fmt.Sprintf("&accountId=%s", accountID) + } + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + orgs := new(PagedDTO) + resp, err := s.client.Do(req, &orgs) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return orgs, resp, nil +} + +// GetOrganizations wraps GetOrganizationsWithContext using the background context. +func (s *ServiceDeskService) GetOrganizations(serviceDeskID interface{}, start int, limit int, accountID string) (*PagedDTO, *Response, error) { + return s.GetOrganizationsWithContext(context.Background(), serviceDeskID, start, limit, accountID) +} + +// AddOrganizationWithContext adds an organization to +// a service desk. If the organization ID is already +// associated with the service desk, no change is made +// and the resource returns a 204 success code. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-post +// Caller must close resp.Body +func (s *ServiceDeskService) AddOrganizationWithContext(ctx context.Context, serviceDeskID interface{}, organizationID int) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/organization", serviceDeskID) + + organization := ServiceDeskOrganizationDTO{ + OrganizationID: organizationID, + } + + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, organization) + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// AddOrganization wraps AddOrganizationWithContext using the background context. +// Caller must close resp.Body +func (s *ServiceDeskService) AddOrganization(serviceDeskID interface{}, organizationID int) (*Response, error) { + return s.AddOrganizationWithContext(context.Background(), serviceDeskID, organizationID) +} + +// RemoveOrganizationWithContext removes an organization +// from a service desk. If the organization ID does not +// match an organization associated with the service desk, +// no change is made and the resource returns a 204 success code. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-delete +// Caller must close resp.Body +func (s *ServiceDeskService) RemoveOrganizationWithContext(ctx context.Context, serviceDeskID interface{}, organizationID int) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/organization", serviceDeskID) + + organization := ServiceDeskOrganizationDTO{ + OrganizationID: organizationID, + } + + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, organization) + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// RemoveOrganization wraps RemoveOrganizationWithContext using the background context. +// Caller must close resp.Body +func (s *ServiceDeskService) RemoveOrganization(serviceDeskID interface{}, organizationID int) (*Response, error) { + return s.RemoveOrganizationWithContext(context.Background(), serviceDeskID, organizationID) +} + +// AddCustomersWithContext adds customers to the given service desk. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-customer-post +func (s *ServiceDeskService) AddCustomersWithContext(ctx context.Context, serviceDeskID interface{}, acountIDs ...string) (*Response, error) { + apiEndpoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/customer", serviceDeskID) + + payload := struct { + AccountIDs []string `json:"accountIds"` + }{ + AccountIDs: acountIDs, + } + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, payload) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return resp, NewJiraError(resp, err) + } + + defer resp.Body.Close() + _, _ = io.Copy(ioutil.Discard, resp.Body) + + return resp, nil +} + +// AddCustomers wraps AddCustomersWithContext using the background context. +func (s *ServiceDeskService) AddCustomers(serviceDeskID interface{}, acountIDs ...string) (*Response, error) { + return s.AddCustomersWithContext(context.Background(), serviceDeskID, acountIDs...) +} + +// RemoveCustomersWithContext removes customers to the given service desk. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-customer-delete +func (s *ServiceDeskService) RemoveCustomersWithContext(ctx context.Context, serviceDeskID interface{}, acountIDs ...string) (*Response, error) { + apiEndpoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/customer", serviceDeskID) + + payload := struct { + AccountIDs []string `json:"accountIDs"` + }{ + AccountIDs: acountIDs, + } + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, payload) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return resp, NewJiraError(resp, err) + } + + defer resp.Body.Close() + _, _ = io.Copy(ioutil.Discard, resp.Body) + + return resp, nil +} + +// RemoveCustomers wraps RemoveCustomersWithContext using the background context. +func (s *ServiceDeskService) RemoveCustomers(serviceDeskID interface{}, acountIDs ...string) (*Response, error) { + return s.RemoveCustomersWithContext(context.Background(), serviceDeskID, acountIDs...) +} + +// ListCustomersWithContext lists customers for a ServiceDesk. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-customer-get +func (s *ServiceDeskService) ListCustomersWithContext(ctx context.Context, serviceDeskID interface{}, options *CustomerListOptions) (*CustomerList, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/customer", serviceDeskID) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + // this is an experiemntal endpoint + req.Header.Set("X-ExperimentalApi", "opt-in") + + if options != nil { + q, err := query.Values(options) + if err != nil { + return nil, nil, err + } + req.URL.RawQuery = q.Encode() + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + defer resp.Body.Close() + + customerList := new(CustomerList) + if err := json.NewDecoder(resp.Body).Decode(customerList); err != nil { + return nil, resp, fmt.Errorf("could not unmarshall the data into struct") + } + + return customerList, resp, nil +} + +// ListCustomers wraps ListCustomersWithContext using the background context. +func (s *ServiceDeskService) ListCustomers(serviceDeskID interface{}, options *CustomerListOptions) (*CustomerList, *Response, error) { + return s.ListCustomersWithContext(context.Background(), serviceDeskID, options) +} diff --git a/vendor/github.com/andygrunwald/go-jira/sprint.go b/vendor/github.com/andygrunwald/go-jira/sprint.go index 7e8e697d..f0f98d6f 100644 --- a/vendor/github.com/andygrunwald/go-jira/sprint.go +++ b/vendor/github.com/andygrunwald/go-jira/sprint.go @@ -1,12 +1,13 @@ package jira import ( + "context" "fmt" "github.com/google/go-querystring/query" ) -// SprintService handles sprints in JIRA Agile API. +// SprintService handles sprints in Jira Agile API. // See https://docs.atlassian.com/jira-software/REST/cloud/ type SprintService struct { client *Client @@ -22,17 +23,18 @@ type IssuesInSprintResult struct { Issues []Issue `json:"issues"` } -// MoveIssuesToSprint moves issues to a sprint, for a given sprint Id. +// MoveIssuesToSprintWithContext moves issues to a sprint, for a given sprint Id. // Issues can only be moved to open or active sprints. // The maximum number of issues that can be moved in one operation is 50. // -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-moveIssuesToSprint -func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Response, error) { +// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-moveIssuesToSprint +// Caller must close resp.Body +func (s *SprintService) MoveIssuesToSprintWithContext(ctx context.Context, sprintID int, issueIDs []string) (*Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID) payload := IssuesWrapper{Issues: issueIDs} - req, err := s.client.NewRequest("POST", apiEndpoint, payload) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, payload) if err != nil { return nil, err @@ -45,15 +47,21 @@ func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Re return resp, err } -// GetIssuesForSprint returns all issues in a sprint, for a given sprint Id. +// MoveIssuesToSprint wraps MoveIssuesToSprintWithContext using the background context. +// Caller must close resp.Body +func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Response, error) { + return s.MoveIssuesToSprintWithContext(context.Background(), sprintID, issueIDs) +} + +// GetIssuesForSprintWithContext returns all issues in a sprint, for a given sprint Id. // This only includes issues that the user has permission to view. // By default, the returned issues are ordered by rank. // -// JIRA API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint -func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, error) { +// Jira API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint +func (s *SprintService) GetIssuesForSprintWithContext(ctx context.Context, sprintID int) ([]Issue, *Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err @@ -68,20 +76,25 @@ func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, er return result.Issues, resp, err } -// GetIssue returns a full representation of the issue for the given issue key. -// JIRA will attempt to identify the issue by the issueIdOrKey path parameter. +// GetIssuesForSprint wraps GetIssuesForSprintWithContext using the background context. +func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, error) { + return s.GetIssuesForSprintWithContext(context.Background(), sprintID) +} + +// GetIssueWithContext returns a full representation of the issue for the given issue key. +// Jira will attempt to identify the issue by the issueIdOrKey path parameter. // This can be an issue id, or an issue key. -// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved. +// If the issue cannot be found via an exact match, Jira will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved. // // The given options will be appended to the query string // -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/issue-getIssue +// Jira API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/issue-getIssue // // TODO: create agile service for holding all agile apis' implementation -func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { +func (s *SprintService) GetIssueWithContext(ctx context.Context, issueID string, options *GetQueryOptions) (*Issue, *Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", issueID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err @@ -105,3 +118,8 @@ func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Iss return issue, resp, nil } + +// GetIssue wraps GetIssueWithContext using the background context. +func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { + return s.GetIssueWithContext(context.Background(), issueID, options) +} diff --git a/vendor/github.com/andygrunwald/go-jira/status.go b/vendor/github.com/andygrunwald/go-jira/status.go index 365d9586..a3703929 100644 --- a/vendor/github.com/andygrunwald/go-jira/status.go +++ b/vendor/github.com/andygrunwald/go-jira/status.go @@ -1,15 +1,17 @@ package jira -// StatusService handles staties for the JIRA instance / API. +import "context" + +// StatusService handles staties for the Jira instance / API. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Workflow-statuses +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Workflow-statuses type StatusService struct { client *Client } -// Status represents the current status of a JIRA issue. +// Status represents the current status of a Jira issue. // Typical status are "Open", "In Progress", "Closed", ... -// Status can be user defined in every JIRA instance. +// Status can be user defined in every Jira instance. type Status struct { Self string `json:"self" structs:"self"` Description string `json:"description" structs:"description"` @@ -19,12 +21,12 @@ type Status struct { StatusCategory StatusCategory `json:"statusCategory" structs:"statusCategory"` } -// GetAllStatuses returns a list of all statuses associated with workflows. +// GetAllStatusesWithContext returns a list of all statuses associated with workflows. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-status-get -func (s *StatusService) GetAllStatuses() ([]Status, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-status-get +func (s *StatusService) GetAllStatusesWithContext(ctx context.Context) ([]Status, *Response, error) { apiEndpoint := "rest/api/2/status" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err @@ -38,3 +40,8 @@ func (s *StatusService) GetAllStatuses() ([]Status, *Response, error) { return statusList, resp, nil } + +// GetAllStatuses wraps GetAllStatusesWithContext using the background context. +func (s *StatusService) GetAllStatuses() ([]Status, *Response, error) { + return s.GetAllStatusesWithContext(context.Background()) +} diff --git a/vendor/github.com/andygrunwald/go-jira/statuscategory.go b/vendor/github.com/andygrunwald/go-jira/statuscategory.go index 05db4207..bed5c566 100644 --- a/vendor/github.com/andygrunwald/go-jira/statuscategory.go +++ b/vendor/github.com/andygrunwald/go-jira/statuscategory.go @@ -1,14 +1,16 @@ package jira -// StatusCategoryService handles status categories for the JIRA instance / API. +import "context" + +// StatusCategoryService handles status categories for the Jira instance / API. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Statuscategory +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Statuscategory type StatusCategoryService struct { client *Client } // StatusCategory represents the category a status belongs to. -// Those categories can be user defined in every JIRA instance. +// Those categories can be user defined in every Jira instance. type StatusCategory struct { Self string `json:"self" structs:"self"` ID int `json:"id" structs:"id"` @@ -17,7 +19,7 @@ type StatusCategory struct { ColorName string `json:"colorName" structs:"colorName"` } -// These constants are the keys of the default JIRA status categories +// These constants are the keys of the default Jira status categories const ( StatusCategoryComplete = "done" StatusCategoryInProgress = "indeterminate" @@ -25,12 +27,12 @@ const ( StatusCategoryUndefined = "undefined" ) -// GetList gets all status categories from JIRA +// GetListWithContext gets all status categories from Jira // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-statuscategory-get -func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-statuscategory-get +func (s *StatusCategoryService) GetListWithContext(ctx context.Context) ([]StatusCategory, *Response, error) { apiEndpoint := "rest/api/2/statuscategory" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -42,3 +44,8 @@ func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) { } return statusCategoryList, resp, nil } + +// GetList wraps GetListWithContext using the background context. +func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) { + return s.GetListWithContext(context.Background()) +} diff --git a/vendor/github.com/andygrunwald/go-jira/types.go b/vendor/github.com/andygrunwald/go-jira/types.go new file mode 100644 index 00000000..b99fc1c3 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/types.go @@ -0,0 +1,9 @@ +package jira + +// Bool is a helper routine that allocates a new bool value +// to store v and returns a pointer to it. +func Bool(v bool) *bool { + p := new(bool) + *p = v + return p +} diff --git a/vendor/github.com/andygrunwald/go-jira/user.go b/vendor/github.com/andygrunwald/go-jira/user.go index 9a6cbd20..78d60396 100644 --- a/vendor/github.com/andygrunwald/go-jira/user.go +++ b/vendor/github.com/andygrunwald/go-jira/user.go @@ -1,25 +1,24 @@ package jira import ( + "context" "encoding/json" "fmt" "io/ioutil" ) -// UserService handles users for the JIRA instance / API. +// UserService handles users for the Jira instance / API. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Users type UserService struct { client *Client } -// User represents a JIRA user. +// User represents a Jira user. type User struct { - Self string `json:"self,omitempty" structs:"self,omitempty"` - AccountID string `json:"accountId,omitempty" structs:"accountId,omitempty"` - AccountType string `json:"accountType,omitempty" structs:"accountType,omitempty"` - // TODO: name & key are deprecated, see: - // https://developer.atlassian.com/cloud/jira/platform/api-changes-for-user-privacy-announcement/ + Self string `json:"self,omitempty" structs:"self,omitempty"` + AccountID string `json:"accountId,omitempty" structs:"accountId,omitempty"` + AccountType string `json:"accountType,omitempty" structs:"accountType,omitempty"` Name string `json:"name,omitempty" structs:"name,omitempty"` Key string `json:"key,omitempty" structs:"key,omitempty"` Password string `json:"-"` @@ -47,12 +46,12 @@ type userSearch []userSearchParam type userSearchF func(userSearch) userSearch -// Get gets user info from JIRA +// GetWithContext gets user info from Jira using its Account Id // -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUser -func (s *UserService) Get(username string) (*User, *Response, error) { - apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-get +func (s *UserService) GetWithContext(ctx context.Context, accountId string) (*User, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/user?accountId=%s", accountId) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -65,12 +64,41 @@ func (s *UserService) Get(username string) (*User, *Response, error) { return user, resp, nil } -// Create creates an user in JIRA. +// Get wraps GetWithContext using the background context. +func (s *UserService) Get(accountId string) (*User, *Response, error) { + return s.GetWithContext(context.Background(), accountId) +} + +// GetByAccountIDWithContext gets user info from Jira +// Searching by another parameter that is not accountId is deprecated, +// but this method is kept for backwards compatibility +// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUser +func (s *UserService) GetByAccountIDWithContext(ctx context.Context, accountID string) (*User, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/user?accountId=%s", accountID) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + user := new(User) + resp, err := s.client.Do(req, user) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + return user, resp, nil +} + +// GetByAccountID wraps GetByAccountIDWithContext using the background context. +func (s *UserService) GetByAccountID(accountID string) (*User, *Response, error) { + return s.GetByAccountIDWithContext(context.Background(), accountID) +} + +// CreateWithContext creates an user in Jira. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-createUser -func (s *UserService) Create(user *User) (*User, *Response, error) { +// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-createUser +func (s *UserService) CreateWithContext(ctx context.Context, user *User) (*User, *Response, error) { apiEndpoint := "/rest/api/2/user" - req, err := s.client.NewRequest("POST", apiEndpoint, user) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, user) if err != nil { return nil, nil, err } @@ -84,24 +112,30 @@ func (s *UserService) Create(user *User) (*User, *Response, error) { defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { - e := fmt.Errorf("Could not read the returned data") + e := fmt.Errorf("could not read the returned data") return nil, resp, NewJiraError(resp, e) } err = json.Unmarshal(data, responseUser) if err != nil { - e := fmt.Errorf("Could not unmarshall the data into struct") + e := fmt.Errorf("could not unmarshall the data into struct") return nil, resp, NewJiraError(resp, e) } return responseUser, resp, nil } -// Delete deletes an user from JIRA. +// Create wraps CreateWithContext using the background context. +func (s *UserService) Create(user *User) (*User, *Response, error) { + return s.CreateWithContext(context.Background(), user) +} + +// DeleteWithContext deletes an user from Jira. // Returns http.StatusNoContent on success. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-user-delete -func (s *UserService) Delete(username string) (*Response, error) { - apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-delete +// Caller must close resp.Body +func (s *UserService) DeleteWithContext(ctx context.Context, accountId string) (*Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/user?accountId=%s", accountId) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { return nil, err } @@ -113,12 +147,18 @@ func (s *UserService) Delete(username string) (*Response, error) { return resp, nil } -// GetGroups returns the groups which the user belongs to +// Delete wraps DeleteWithContext using the background context. +// Caller must close resp.Body +func (s *UserService) Delete(accountId string) (*Response, error) { + return s.DeleteWithContext(context.Background(), accountId) +} + +// GetGroupsWithContext returns the groups which the user belongs to // -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUserGroups -func (s *UserService) GetGroups(username string) (*[]UserGroup, *Response, error) { - apiEndpoint := fmt.Sprintf("/rest/api/2/user/groups?username=%s", username) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-groups-get +func (s *UserService) GetGroupsWithContext(ctx context.Context, accountId string) (*[]UserGroup, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/user/groups?accountId=%s", accountId) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -131,12 +171,17 @@ func (s *UserService) GetGroups(username string) (*[]UserGroup, *Response, error return userGroups, resp, nil } -// Get information about the current logged-in user +// GetGroups wraps GetGroupsWithContext using the background context. +func (s *UserService) GetGroups(accountId string) (*[]UserGroup, *Response, error) { + return s.GetGroupsWithContext(context.Background(), accountId) +} + +// GetSelfWithContext information about the current logged-in user // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-myself-get -func (s *UserService) GetSelf() (*User, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-myself-get +func (s *UserService) GetSelfWithContext(ctx context.Context) (*User, *Response, error) { const apiEndpoint = "rest/api/2/myself" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -148,6 +193,11 @@ func (s *UserService) GetSelf() (*User, *Response, error) { return &user, resp, nil } +// GetSelf wraps GetSelfWithContext using the background context. +func (s *UserService) GetSelf() (*User, *Response, error) { + return s.GetSelfWithContext(context.Background()) +} + // WithMaxResults sets the max results to return func WithMaxResults(maxResults int) userSearchF { return func(s userSearch) userSearch { @@ -180,14 +230,38 @@ func WithInactive(inactive bool) userSearchF { } } -// Find searches for user info from JIRA: -// It can find users by email, username or name +// WithUsername sets the username to search +func WithUsername(username string) userSearchF { + return func(s userSearch) userSearch { + s = append(s, userSearchParam{name: "username", value: username}) + return s + } +} + +// WithAccountId sets the account id to search +func WithAccountId(accountId string) userSearchF { + return func(s userSearch) userSearch { + s = append(s, userSearchParam{name: "accountId", value: accountId}) + return s + } +} + +// WithProperty sets the property (Property keys are specified by path) to search +func WithProperty(property string) userSearchF { + return func(s userSearch) userSearch { + s = append(s, userSearchParam{name: "property", value: property}) + return s + } +} + +// FindWithContext searches for user info from Jira: +// It can find users by email or display name using the query parameter // -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-findUsers -func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-search-get +func (s *UserService) FindWithContext(ctx context.Context, property string, tweaks ...userSearchF) ([]User, *Response, error) { search := []userSearchParam{ { - name: "username", + name: "query", value: property, }, } @@ -201,7 +275,7 @@ func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Res } apiEndpoint := fmt.Sprintf("/rest/api/2/user/search?%s", queryString[:len(queryString)-1]) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -213,3 +287,8 @@ func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Res } return users, resp, nil } + +// Find wraps FindWithContext using the background context. +func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Response, error) { + return s.FindWithContext(context.Background(), property, tweaks...) +} diff --git a/vendor/github.com/andygrunwald/go-jira/version.go b/vendor/github.com/andygrunwald/go-jira/version.go index 2af81f47..bca12a45 100644 --- a/vendor/github.com/andygrunwald/go-jira/version.go +++ b/vendor/github.com/andygrunwald/go-jira/version.go @@ -1,14 +1,15 @@ package jira import ( + "context" "encoding/json" "fmt" "io/ioutil" ) -// VersionService handles Versions for the JIRA instance / API. +// VersionService handles Versions for the Jira instance / API. // -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/version +// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/version type VersionService struct { client *Client } @@ -19,20 +20,20 @@ type Version struct { ID string `json:"id,omitempty" structs:"id,omitempty"` Name string `json:"name,omitempty" structs:"name,omitempty"` Description string `json:"description,omitempty" structs:"description,omitempty"` - Archived bool `json:"archived,omitempty" structs:"archived,omitempty"` - Released bool `json:"released,omitempty" structs:"released,omitempty"` + Archived *bool `json:"archived,omitempty" structs:"archived,omitempty"` + Released *bool `json:"released,omitempty" structs:"released,omitempty"` ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"` UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"userReleaseDate,omitempty"` ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` // Unlike other IDs, this is returned as a number StartDate string `json:"startDate,omitempty" structs:"startDate,omitempty"` } -// Get gets version info from JIRA +// GetWithContext gets version info from Jira // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-get -func (s *VersionService) Get(versionID int) (*Version, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-get +func (s *VersionService) GetWithContext(ctx context.Context, versionID int) (*Version, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/version/%v", versionID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -45,12 +46,17 @@ func (s *VersionService) Get(versionID int) (*Version, *Response, error) { return version, resp, nil } -// Create creates a version in JIRA. +// Get wraps GetWithContext using the background context. +func (s *VersionService) Get(versionID int) (*Version, *Response, error) { + return s.GetWithContext(context.Background(), versionID) +} + +// CreateWithContext creates a version in Jira. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-post -func (s *VersionService) Create(version *Version) (*Version, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-post +func (s *VersionService) CreateWithContext(ctx context.Context, version *Version) (*Version, *Response, error) { apiEndpoint := "/rest/api/2/version" - req, err := s.client.NewRequest("POST", apiEndpoint, version) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, version) if err != nil { return nil, nil, err } @@ -64,23 +70,29 @@ func (s *VersionService) Create(version *Version) (*Version, *Response, error) { defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { - e := fmt.Errorf("Could not read the returned data") + e := fmt.Errorf("could not read the returned data") return nil, resp, NewJiraError(resp, e) } err = json.Unmarshal(data, responseVersion) if err != nil { - e := fmt.Errorf("Could not unmarshall the data into struct") + e := fmt.Errorf("could not unmarshall the data into struct") return nil, resp, NewJiraError(resp, e) } return responseVersion, resp, nil } -// Update updates a version from a JSON representation. +// Create wraps CreateWithContext using the background context. +func (s *VersionService) Create(version *Version) (*Version, *Response, error) { + return s.CreateWithContext(context.Background(), version) +} + +// UpdateWithContext updates a version from a JSON representation. // -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-put -func (s *VersionService) Update(version *Version) (*Version, *Response, error) { +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-put +// Caller must close resp.Body +func (s *VersionService) UpdateWithContext(ctx context.Context, version *Version) (*Version, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/version/%v", version.ID) - req, err := s.client.NewRequest("PUT", apiEndpoint, version) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, version) if err != nil { return nil, nil, err } @@ -95,3 +107,9 @@ func (s *VersionService) Update(version *Version) (*Version, *Response, error) { ret := *version return &ret, resp, nil } + +// Update wraps UpdateWithContext using the background context. +// Caller must close resp.Body +func (s *VersionService) Update(version *Version) (*Version, *Response, error) { + return s.UpdateWithContext(context.Background(), version) +} diff --git a/vendor/github.com/denisenkom/go-mssqldb/log.go b/vendor/github.com/denisenkom/go-mssqldb/log.go deleted file mode 100644 index 9b8c551e..00000000 --- a/vendor/github.com/denisenkom/go-mssqldb/log.go +++ /dev/null @@ -1,30 +0,0 @@ -package mssql - -import ( - "log" -) - -type Logger interface { - Printf(format string, v ...interface{}) - Println(v ...interface{}) -} - -type optionalLogger struct { - logger Logger -} - -func (o optionalLogger) Printf(format string, v ...interface{}) { - if o.logger != nil { - o.logger.Printf(format, v...) - } else { - log.Printf(format, v...) - } -} - -func (o optionalLogger) Println(v ...interface{}) { - if o.logger != nil { - o.logger.Println(v...) - } else { - log.Println(v...) - } -} diff --git a/vendor/github.com/fatih/structs/.travis.yml b/vendor/github.com/fatih/structs/.travis.yml index cbf2ccc9..a08df798 100644 --- a/vendor/github.com/fatih/structs/.travis.yml +++ b/vendor/github.com/fatih/structs/.travis.yml @@ -1,6 +1,8 @@ language: go go: - 1.7.x + - 1.8.x + - 1.9.x - tip sudo: false before_install: diff --git a/vendor/github.com/fatih/structs/README.md b/vendor/github.com/fatih/structs/README.md index 44e01006..a75eabf3 100644 --- a/vendor/github.com/fatih/structs/README.md +++ b/vendor/github.com/fatih/structs/README.md @@ -81,8 +81,8 @@ n := s.Names() // Get a []string f := s.Field(name) // Get a *Field based on the given field name f, ok := s.FieldOk(name) // Get a *Field based on the given field name n := s.Name() // Get the struct name -h := s.HasZero() // Check if any field is initialized -z := s.IsZero() // Check if all fields are initialized +h := s.HasZero() // Check if any field is uninitialized +z := s.IsZero() // Check if all fields are uninitialized ``` ### Field methods diff --git a/vendor/github.com/fatih/structs/structs.go b/vendor/github.com/fatih/structs/structs.go index be3816ab..3a877065 100644 --- a/vendor/github.com/fatih/structs/structs.go +++ b/vendor/github.com/fatih/structs/structs.go @@ -203,9 +203,7 @@ func (s *Struct) Values() []interface{} { if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { // look out for embedded structs, and convert them to a // []interface{} to be added to the final values slice - for _, embeddedVal := range Values(val.Interface()) { - t = append(t, embeddedVal) - } + t = append(t, Values(val.Interface())...) } else { t = append(t, val.Interface()) } @@ -573,7 +571,7 @@ func (s *Struct) nested(val reflect.Value) interface{} { break } - slices := make([]interface{}, val.Len(), val.Len()) + slices := make([]interface{}, val.Len()) for x := 0; x < val.Len(); x++ { slices[x] = s.nested(val.Index(x)) } diff --git a/vendor/github.com/fatih/structs/tags.go b/vendor/github.com/fatih/structs/tags.go index 8859341c..136a31eb 100644 --- a/vendor/github.com/fatih/structs/tags.go +++ b/vendor/github.com/fatih/structs/tags.go @@ -5,7 +5,7 @@ import "strings" // tagOptions contains a slice of tag options type tagOptions []string -// Has returns true if the given optiton is available in tagOptions +// Has returns true if the given option is available in tagOptions func (t tagOptions) Has(opt string) bool { for _, tagOpt := range t { if tagOpt == opt { diff --git a/vendor/github.com/felixge/httpsnoop/.gitignore b/vendor/github.com/felixge/httpsnoop/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/vendor/github.com/felixge/httpsnoop/.travis.yml b/vendor/github.com/felixge/httpsnoop/.travis.yml new file mode 100644 index 00000000..bfc42120 --- /dev/null +++ b/vendor/github.com/felixge/httpsnoop/.travis.yml @@ -0,0 +1,6 @@ +language: go + +go: + - 1.6 + - 1.7 + - 1.8 diff --git a/vendor/github.com/felixge/httpsnoop/LICENSE.txt b/vendor/github.com/felixge/httpsnoop/LICENSE.txt new file mode 100644 index 00000000..e028b46a --- /dev/null +++ b/vendor/github.com/felixge/httpsnoop/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2016 Felix Geisendörfer (felix@debuggable.com) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. diff --git a/vendor/github.com/felixge/httpsnoop/Makefile b/vendor/github.com/felixge/httpsnoop/Makefile new file mode 100644 index 00000000..2d84889a --- /dev/null +++ b/vendor/github.com/felixge/httpsnoop/Makefile @@ -0,0 +1,10 @@ +.PHONY: ci generate clean + +ci: clean generate + go test -v ./... + +generate: + go generate . + +clean: + rm -rf *_generated*.go diff --git a/vendor/github.com/felixge/httpsnoop/README.md b/vendor/github.com/felixge/httpsnoop/README.md new file mode 100644 index 00000000..ddcecd13 --- /dev/null +++ b/vendor/github.com/felixge/httpsnoop/README.md @@ -0,0 +1,95 @@ +# httpsnoop + +Package httpsnoop provides an easy way to capture http related metrics (i.e. +response time, bytes written, and http status code) from your application's +http.Handlers. + +Doing this requires non-trivial wrapping of the http.ResponseWriter interface, +which is also exposed for users interested in a more low-level API. + +[![GoDoc](https://godoc.org/github.com/felixge/httpsnoop?status.svg)](https://godoc.org/github.com/felixge/httpsnoop) +[![Build Status](https://travis-ci.org/felixge/httpsnoop.svg?branch=master)](https://travis-ci.org/felixge/httpsnoop) + +## Usage Example + +```go +// myH is your app's http handler, perhaps a http.ServeMux or similar. +var myH http.Handler +// wrappedH wraps myH in order to log every request. +wrappedH := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + m := httpsnoop.CaptureMetrics(myH, w, r) + log.Printf( + "%s %s (code=%d dt=%s written=%d)", + r.Method, + r.URL, + m.Code, + m.Duration, + m.Written, + ) +}) +http.ListenAndServe(":8080", wrappedH) +``` + +## Why this package exists + +Instrumenting an application's http.Handler is surprisingly difficult. + +However if you google for e.g. "capture ResponseWriter status code" you'll find +lots of advise and code examples that suggest it to be a fairly trivial +undertaking. Unfortunately everything I've seen so far has a high chance of +breaking your application. + +The main problem is that a `http.ResponseWriter` often implements additional +interfaces such as `http.Flusher`, `http.CloseNotifier`, `http.Hijacker`, `http.Pusher`, and +`io.ReaderFrom`. So the naive approach of just wrapping `http.ResponseWriter` +in your own struct that also implements the `http.ResponseWriter` interface +will hide the additional interfaces mentioned above. This has a high change of +introducing subtle bugs into any non-trivial application. + +Another approach I've seen people take is to return a struct that implements +all of the interfaces above. However, that's also problematic, because it's +difficult to fake some of these interfaces behaviors when the underlying +`http.ResponseWriter` doesn't have an implementation. It's also dangerous, +because an application may choose to operate differently, merely because it +detects the presence of these additional interfaces. + +This package solves this problem by checking which additional interfaces a +`http.ResponseWriter` implements, returning a wrapped version implementing the +exact same set of interfaces. + +Additionally this package properly handles edge cases such as `WriteHeader` not +being called, or called more than once, as well as concurrent calls to +`http.ResponseWriter` methods, and even calls happening after the wrapped +`ServeHTTP` has already returned. + +Unfortunately this package is not perfect either. It's possible that it is +still missing some interfaces provided by the go core (let me know if you find +one), and it won't work for applications adding their own interfaces into the +mix. You can however use `httpsnoop.Unwrap(w)` to access the underlying +`http.ResponseWriter` and type-assert the result to its other interfaces. + +However, hopefully the explanation above has sufficiently scared you of rolling +your own solution to this problem. httpsnoop may still break your application, +but at least it tries to avoid it as much as possible. + +Anyway, the real problem here is that smuggling additional interfaces inside +`http.ResponseWriter` is a problematic design choice, but it probably goes as +deep as the Go language specification itself. But that's okay, I still prefer +Go over the alternatives ;). + +## Performance + +``` +BenchmarkBaseline-8 20000 94912 ns/op +BenchmarkCaptureMetrics-8 20000 95461 ns/op +``` + +As you can see, using `CaptureMetrics` on a vanilla http.Handler introduces an +overhead of ~500 ns per http request on my machine. However, the margin of +error appears to be larger than that, therefor it should be reasonable to +assume that the overhead introduced by `CaptureMetrics` is absolutely +negligible. + +## License + +MIT diff --git a/vendor/github.com/felixge/httpsnoop/capture_metrics.go b/vendor/github.com/felixge/httpsnoop/capture_metrics.go new file mode 100644 index 00000000..b77cc7c0 --- /dev/null +++ b/vendor/github.com/felixge/httpsnoop/capture_metrics.go @@ -0,0 +1,86 @@ +package httpsnoop + +import ( + "io" + "net/http" + "time" +) + +// Metrics holds metrics captured from CaptureMetrics. +type Metrics struct { + // Code is the first http response code passed to the WriteHeader func of + // the ResponseWriter. If no such call is made, a default code of 200 is + // assumed instead. + Code int + // Duration is the time it took to execute the handler. + Duration time.Duration + // Written is the number of bytes successfully written by the Write or + // ReadFrom function of the ResponseWriter. ResponseWriters may also write + // data to their underlaying connection directly (e.g. headers), but those + // are not tracked. Therefor the number of Written bytes will usually match + // the size of the response body. + Written int64 +} + +// CaptureMetrics wraps the given hnd, executes it with the given w and r, and +// returns the metrics it captured from it. +func CaptureMetrics(hnd http.Handler, w http.ResponseWriter, r *http.Request) Metrics { + return CaptureMetricsFn(w, func(ww http.ResponseWriter) { + hnd.ServeHTTP(ww, r) + }) +} + +// CaptureMetricsFn wraps w and calls fn with the wrapped w and returns the +// resulting metrics. This is very similar to CaptureMetrics (which is just +// sugar on top of this func), but is a more usable interface if your +// application doesn't use the Go http.Handler interface. +func CaptureMetricsFn(w http.ResponseWriter, fn func(http.ResponseWriter)) Metrics { + m := Metrics{Code: http.StatusOK} + m.CaptureMetrics(w, fn) + return m +} + +// CaptureMetrics wraps w and calls fn with the wrapped w and updates +// Metrics m with the resulting metrics. This is similar to CaptureMetricsFn, +// but allows one to customize starting Metrics object. +func (m *Metrics) CaptureMetrics(w http.ResponseWriter, fn func(http.ResponseWriter)) { + var ( + start = time.Now() + headerWritten bool + hooks = Hooks{ + WriteHeader: func(next WriteHeaderFunc) WriteHeaderFunc { + return func(code int) { + next(code) + + if !headerWritten { + m.Code = code + headerWritten = true + } + } + }, + + Write: func(next WriteFunc) WriteFunc { + return func(p []byte) (int, error) { + n, err := next(p) + + m.Written += int64(n) + headerWritten = true + return n, err + } + }, + + ReadFrom: func(next ReadFromFunc) ReadFromFunc { + return func(src io.Reader) (int64, error) { + n, err := next(src) + + headerWritten = true + m.Written += n + return n, err + } + }, + } + ) + + fn(Wrap(w, hooks)) + m.Duration += time.Since(start) +} diff --git a/vendor/github.com/felixge/httpsnoop/docs.go b/vendor/github.com/felixge/httpsnoop/docs.go new file mode 100644 index 00000000..203c35b3 --- /dev/null +++ b/vendor/github.com/felixge/httpsnoop/docs.go @@ -0,0 +1,10 @@ +// Package httpsnoop provides an easy way to capture http related metrics (i.e. +// response time, bytes written, and http status code) from your application's +// http.Handlers. +// +// Doing this requires non-trivial wrapping of the http.ResponseWriter +// interface, which is also exposed for users interested in a more low-level +// API. +package httpsnoop + +//go:generate go run codegen/main.go diff --git a/vendor/github.com/felixge/httpsnoop/wrap_generated_gteq_1.8.go b/vendor/github.com/felixge/httpsnoop/wrap_generated_gteq_1.8.go new file mode 100644 index 00000000..31cbdfb8 --- /dev/null +++ b/vendor/github.com/felixge/httpsnoop/wrap_generated_gteq_1.8.go @@ -0,0 +1,436 @@ +// +build go1.8 +// Code generated by "httpsnoop/codegen"; DO NOT EDIT + +package httpsnoop + +import ( + "bufio" + "io" + "net" + "net/http" +) + +// HeaderFunc is part of the http.ResponseWriter interface. +type HeaderFunc func() http.Header + +// WriteHeaderFunc is part of the http.ResponseWriter interface. +type WriteHeaderFunc func(code int) + +// WriteFunc is part of the http.ResponseWriter interface. +type WriteFunc func(b []byte) (int, error) + +// FlushFunc is part of the http.Flusher interface. +type FlushFunc func() + +// CloseNotifyFunc is part of the http.CloseNotifier interface. +type CloseNotifyFunc func() <-chan bool + +// HijackFunc is part of the http.Hijacker interface. +type HijackFunc func() (net.Conn, *bufio.ReadWriter, error) + +// ReadFromFunc is part of the io.ReaderFrom interface. +type ReadFromFunc func(src io.Reader) (int64, error) + +// PushFunc is part of the http.Pusher interface. +type PushFunc func(target string, opts *http.PushOptions) error + +// Hooks defines a set of method interceptors for methods included in +// http.ResponseWriter as well as some others. You can think of them as +// middleware for the function calls they target. See Wrap for more details. +type Hooks struct { + Header func(HeaderFunc) HeaderFunc + WriteHeader func(WriteHeaderFunc) WriteHeaderFunc + Write func(WriteFunc) WriteFunc + Flush func(FlushFunc) FlushFunc + CloseNotify func(CloseNotifyFunc) CloseNotifyFunc + Hijack func(HijackFunc) HijackFunc + ReadFrom func(ReadFromFunc) ReadFromFunc + Push func(PushFunc) PushFunc +} + +// Wrap returns a wrapped version of w that provides the exact same interface +// as w. Specifically if w implements any combination of: +// +// - http.Flusher +// - http.CloseNotifier +// - http.Hijacker +// - io.ReaderFrom +// - http.Pusher +// +// The wrapped version will implement the exact same combination. If no hooks +// are set, the wrapped version also behaves exactly as w. Hooks targeting +// methods not supported by w are ignored. Any other hooks will intercept the +// method they target and may modify the call's arguments and/or return values. +// The CaptureMetrics implementation serves as a working example for how the +// hooks can be used. +func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter { + rw := &rw{w: w, h: hooks} + _, i0 := w.(http.Flusher) + _, i1 := w.(http.CloseNotifier) + _, i2 := w.(http.Hijacker) + _, i3 := w.(io.ReaderFrom) + _, i4 := w.(http.Pusher) + switch { + // combination 1/32 + case !i0 && !i1 && !i2 && !i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + }{rw, rw} + // combination 2/32 + case !i0 && !i1 && !i2 && !i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.Pusher + }{rw, rw, rw} + // combination 3/32 + case !i0 && !i1 && !i2 && i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + io.ReaderFrom + }{rw, rw, rw} + // combination 4/32 + case !i0 && !i1 && !i2 && i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + io.ReaderFrom + http.Pusher + }{rw, rw, rw, rw} + // combination 5/32 + case !i0 && !i1 && i2 && !i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.Hijacker + }{rw, rw, rw} + // combination 6/32 + case !i0 && !i1 && i2 && !i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.Hijacker + http.Pusher + }{rw, rw, rw, rw} + // combination 7/32 + case !i0 && !i1 && i2 && i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.Hijacker + io.ReaderFrom + }{rw, rw, rw, rw} + // combination 8/32 + case !i0 && !i1 && i2 && i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.Hijacker + io.ReaderFrom + http.Pusher + }{rw, rw, rw, rw, rw} + // combination 9/32 + case !i0 && i1 && !i2 && !i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.CloseNotifier + }{rw, rw, rw} + // combination 10/32 + case !i0 && i1 && !i2 && !i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.CloseNotifier + http.Pusher + }{rw, rw, rw, rw} + // combination 11/32 + case !i0 && i1 && !i2 && i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.CloseNotifier + io.ReaderFrom + }{rw, rw, rw, rw} + // combination 12/32 + case !i0 && i1 && !i2 && i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.CloseNotifier + io.ReaderFrom + http.Pusher + }{rw, rw, rw, rw, rw} + // combination 13/32 + case !i0 && i1 && i2 && !i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.CloseNotifier + http.Hijacker + }{rw, rw, rw, rw} + // combination 14/32 + case !i0 && i1 && i2 && !i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.CloseNotifier + http.Hijacker + http.Pusher + }{rw, rw, rw, rw, rw} + // combination 15/32 + case !i0 && i1 && i2 && i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.CloseNotifier + http.Hijacker + io.ReaderFrom + }{rw, rw, rw, rw, rw} + // combination 16/32 + case !i0 && i1 && i2 && i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.CloseNotifier + http.Hijacker + io.ReaderFrom + http.Pusher + }{rw, rw, rw, rw, rw, rw} + // combination 17/32 + case i0 && !i1 && !i2 && !i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + }{rw, rw, rw} + // combination 18/32 + case i0 && !i1 && !i2 && !i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.Pusher + }{rw, rw, rw, rw} + // combination 19/32 + case i0 && !i1 && !i2 && i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + io.ReaderFrom + }{rw, rw, rw, rw} + // combination 20/32 + case i0 && !i1 && !i2 && i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + io.ReaderFrom + http.Pusher + }{rw, rw, rw, rw, rw} + // combination 21/32 + case i0 && !i1 && i2 && !i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.Hijacker + }{rw, rw, rw, rw} + // combination 22/32 + case i0 && !i1 && i2 && !i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.Hijacker + http.Pusher + }{rw, rw, rw, rw, rw} + // combination 23/32 + case i0 && !i1 && i2 && i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.Hijacker + io.ReaderFrom + }{rw, rw, rw, rw, rw} + // combination 24/32 + case i0 && !i1 && i2 && i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.Hijacker + io.ReaderFrom + http.Pusher + }{rw, rw, rw, rw, rw, rw} + // combination 25/32 + case i0 && i1 && !i2 && !i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.CloseNotifier + }{rw, rw, rw, rw} + // combination 26/32 + case i0 && i1 && !i2 && !i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.CloseNotifier + http.Pusher + }{rw, rw, rw, rw, rw} + // combination 27/32 + case i0 && i1 && !i2 && i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.CloseNotifier + io.ReaderFrom + }{rw, rw, rw, rw, rw} + // combination 28/32 + case i0 && i1 && !i2 && i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.CloseNotifier + io.ReaderFrom + http.Pusher + }{rw, rw, rw, rw, rw, rw} + // combination 29/32 + case i0 && i1 && i2 && !i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.CloseNotifier + http.Hijacker + }{rw, rw, rw, rw, rw} + // combination 30/32 + case i0 && i1 && i2 && !i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.CloseNotifier + http.Hijacker + http.Pusher + }{rw, rw, rw, rw, rw, rw} + // combination 31/32 + case i0 && i1 && i2 && i3 && !i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.CloseNotifier + http.Hijacker + io.ReaderFrom + }{rw, rw, rw, rw, rw, rw} + // combination 32/32 + case i0 && i1 && i2 && i3 && i4: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.CloseNotifier + http.Hijacker + io.ReaderFrom + http.Pusher + }{rw, rw, rw, rw, rw, rw, rw} + } + panic("unreachable") +} + +type rw struct { + w http.ResponseWriter + h Hooks +} + +func (w *rw) Unwrap() http.ResponseWriter { + return w.w +} + +func (w *rw) Header() http.Header { + f := w.w.(http.ResponseWriter).Header + if w.h.Header != nil { + f = w.h.Header(f) + } + return f() +} + +func (w *rw) WriteHeader(code int) { + f := w.w.(http.ResponseWriter).WriteHeader + if w.h.WriteHeader != nil { + f = w.h.WriteHeader(f) + } + f(code) +} + +func (w *rw) Write(b []byte) (int, error) { + f := w.w.(http.ResponseWriter).Write + if w.h.Write != nil { + f = w.h.Write(f) + } + return f(b) +} + +func (w *rw) Flush() { + f := w.w.(http.Flusher).Flush + if w.h.Flush != nil { + f = w.h.Flush(f) + } + f() +} + +func (w *rw) CloseNotify() <-chan bool { + f := w.w.(http.CloseNotifier).CloseNotify + if w.h.CloseNotify != nil { + f = w.h.CloseNotify(f) + } + return f() +} + +func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) { + f := w.w.(http.Hijacker).Hijack + if w.h.Hijack != nil { + f = w.h.Hijack(f) + } + return f() +} + +func (w *rw) ReadFrom(src io.Reader) (int64, error) { + f := w.w.(io.ReaderFrom).ReadFrom + if w.h.ReadFrom != nil { + f = w.h.ReadFrom(f) + } + return f(src) +} + +func (w *rw) Push(target string, opts *http.PushOptions) error { + f := w.w.(http.Pusher).Push + if w.h.Push != nil { + f = w.h.Push(f) + } + return f(target, opts) +} + +type Unwrapper interface { + Unwrap() http.ResponseWriter +} + +// Unwrap returns the underlying http.ResponseWriter from within zero or more +// layers of httpsnoop wrappers. +func Unwrap(w http.ResponseWriter) http.ResponseWriter { + if rw, ok := w.(Unwrapper); ok { + // recurse until rw.Unwrap() returns a non-Unwrapper + return Unwrap(rw.Unwrap()) + } else { + return w + } +} diff --git a/vendor/github.com/felixge/httpsnoop/wrap_generated_lt_1.8.go b/vendor/github.com/felixge/httpsnoop/wrap_generated_lt_1.8.go new file mode 100644 index 00000000..ab99c07c --- /dev/null +++ b/vendor/github.com/felixge/httpsnoop/wrap_generated_lt_1.8.go @@ -0,0 +1,278 @@ +// +build !go1.8 +// Code generated by "httpsnoop/codegen"; DO NOT EDIT + +package httpsnoop + +import ( + "bufio" + "io" + "net" + "net/http" +) + +// HeaderFunc is part of the http.ResponseWriter interface. +type HeaderFunc func() http.Header + +// WriteHeaderFunc is part of the http.ResponseWriter interface. +type WriteHeaderFunc func(code int) + +// WriteFunc is part of the http.ResponseWriter interface. +type WriteFunc func(b []byte) (int, error) + +// FlushFunc is part of the http.Flusher interface. +type FlushFunc func() + +// CloseNotifyFunc is part of the http.CloseNotifier interface. +type CloseNotifyFunc func() <-chan bool + +// HijackFunc is part of the http.Hijacker interface. +type HijackFunc func() (net.Conn, *bufio.ReadWriter, error) + +// ReadFromFunc is part of the io.ReaderFrom interface. +type ReadFromFunc func(src io.Reader) (int64, error) + +// Hooks defines a set of method interceptors for methods included in +// http.ResponseWriter as well as some others. You can think of them as +// middleware for the function calls they target. See Wrap for more details. +type Hooks struct { + Header func(HeaderFunc) HeaderFunc + WriteHeader func(WriteHeaderFunc) WriteHeaderFunc + Write func(WriteFunc) WriteFunc + Flush func(FlushFunc) FlushFunc + CloseNotify func(CloseNotifyFunc) CloseNotifyFunc + Hijack func(HijackFunc) HijackFunc + ReadFrom func(ReadFromFunc) ReadFromFunc +} + +// Wrap returns a wrapped version of w that provides the exact same interface +// as w. Specifically if w implements any combination of: +// +// - http.Flusher +// - http.CloseNotifier +// - http.Hijacker +// - io.ReaderFrom +// +// The wrapped version will implement the exact same combination. If no hooks +// are set, the wrapped version also behaves exactly as w. Hooks targeting +// methods not supported by w are ignored. Any other hooks will intercept the +// method they target and may modify the call's arguments and/or return values. +// The CaptureMetrics implementation serves as a working example for how the +// hooks can be used. +func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter { + rw := &rw{w: w, h: hooks} + _, i0 := w.(http.Flusher) + _, i1 := w.(http.CloseNotifier) + _, i2 := w.(http.Hijacker) + _, i3 := w.(io.ReaderFrom) + switch { + // combination 1/16 + case !i0 && !i1 && !i2 && !i3: + return struct { + Unwrapper + http.ResponseWriter + }{rw, rw} + // combination 2/16 + case !i0 && !i1 && !i2 && i3: + return struct { + Unwrapper + http.ResponseWriter + io.ReaderFrom + }{rw, rw, rw} + // combination 3/16 + case !i0 && !i1 && i2 && !i3: + return struct { + Unwrapper + http.ResponseWriter + http.Hijacker + }{rw, rw, rw} + // combination 4/16 + case !i0 && !i1 && i2 && i3: + return struct { + Unwrapper + http.ResponseWriter + http.Hijacker + io.ReaderFrom + }{rw, rw, rw, rw} + // combination 5/16 + case !i0 && i1 && !i2 && !i3: + return struct { + Unwrapper + http.ResponseWriter + http.CloseNotifier + }{rw, rw, rw} + // combination 6/16 + case !i0 && i1 && !i2 && i3: + return struct { + Unwrapper + http.ResponseWriter + http.CloseNotifier + io.ReaderFrom + }{rw, rw, rw, rw} + // combination 7/16 + case !i0 && i1 && i2 && !i3: + return struct { + Unwrapper + http.ResponseWriter + http.CloseNotifier + http.Hijacker + }{rw, rw, rw, rw} + // combination 8/16 + case !i0 && i1 && i2 && i3: + return struct { + Unwrapper + http.ResponseWriter + http.CloseNotifier + http.Hijacker + io.ReaderFrom + }{rw, rw, rw, rw, rw} + // combination 9/16 + case i0 && !i1 && !i2 && !i3: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + }{rw, rw, rw} + // combination 10/16 + case i0 && !i1 && !i2 && i3: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + io.ReaderFrom + }{rw, rw, rw, rw} + // combination 11/16 + case i0 && !i1 && i2 && !i3: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.Hijacker + }{rw, rw, rw, rw} + // combination 12/16 + case i0 && !i1 && i2 && i3: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.Hijacker + io.ReaderFrom + }{rw, rw, rw, rw, rw} + // combination 13/16 + case i0 && i1 && !i2 && !i3: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.CloseNotifier + }{rw, rw, rw, rw} + // combination 14/16 + case i0 && i1 && !i2 && i3: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.CloseNotifier + io.ReaderFrom + }{rw, rw, rw, rw, rw} + // combination 15/16 + case i0 && i1 && i2 && !i3: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.CloseNotifier + http.Hijacker + }{rw, rw, rw, rw, rw} + // combination 16/16 + case i0 && i1 && i2 && i3: + return struct { + Unwrapper + http.ResponseWriter + http.Flusher + http.CloseNotifier + http.Hijacker + io.ReaderFrom + }{rw, rw, rw, rw, rw, rw} + } + panic("unreachable") +} + +type rw struct { + w http.ResponseWriter + h Hooks +} + +func (w *rw) Unwrap() http.ResponseWriter { + return w.w +} + +func (w *rw) Header() http.Header { + f := w.w.(http.ResponseWriter).Header + if w.h.Header != nil { + f = w.h.Header(f) + } + return f() +} + +func (w *rw) WriteHeader(code int) { + f := w.w.(http.ResponseWriter).WriteHeader + if w.h.WriteHeader != nil { + f = w.h.WriteHeader(f) + } + f(code) +} + +func (w *rw) Write(b []byte) (int, error) { + f := w.w.(http.ResponseWriter).Write + if w.h.Write != nil { + f = w.h.Write(f) + } + return f(b) +} + +func (w *rw) Flush() { + f := w.w.(http.Flusher).Flush + if w.h.Flush != nil { + f = w.h.Flush(f) + } + f() +} + +func (w *rw) CloseNotify() <-chan bool { + f := w.w.(http.CloseNotifier).CloseNotify + if w.h.CloseNotify != nil { + f = w.h.CloseNotify(f) + } + return f() +} + +func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) { + f := w.w.(http.Hijacker).Hijack + if w.h.Hijack != nil { + f = w.h.Hijack(f) + } + return f() +} + +func (w *rw) ReadFrom(src io.Reader) (int64, error) { + f := w.w.(io.ReaderFrom).ReadFrom + if w.h.ReadFrom != nil { + f = w.h.ReadFrom(f) + } + return f(src) +} + +type Unwrapper interface { + Unwrap() http.ResponseWriter +} + +// Unwrap returns the underlying http.ResponseWriter from within zero or more +// layers of httpsnoop wrappers. +func Unwrap(w http.ResponseWriter) http.ResponseWriter { + if rw, ok := w.(Unwrapper); ok { + // recurse until rw.Unwrap() returns a non-Unwrapper + return Unwrap(rw.Unwrap()) + } else { + return w + } +} diff --git a/vendor/github.com/go-asn1-ber/asn1-ber/.travis.yml b/vendor/github.com/go-asn1-ber/asn1-ber/.travis.yml deleted file mode 100644 index 764b5418..00000000 --- a/vendor/github.com/go-asn1-ber/asn1-ber/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -language: go - -go: - - 1.2.x - - 1.6.x - - 1.9.x - - 1.10.x - - 1.11.x - - 1.12.x - - 1.14.x - - tip - -os: - - linux - -arch: - - amd64 - - ppc64le - -dist: xenial - -env: - - GOARCH=amd64 - -jobs: - include: - - os: windows - go: 1.14.x - - os: osx - go: 1.14.x - - os: linux - go: 1.14.x - arch: arm64 - - os: linux - go: 1.14.x - env: - - GOARCH=386 - -script: - - go test -v -cover ./... || go test -v ./... -matrix: - allowfailures: - go: 1.2.x diff --git a/vendor/github.com/go-asn1-ber/asn1-ber/ber.go b/vendor/github.com/go-asn1-ber/asn1-ber/ber.go index 4fd7a66e..bafa7863 100644 --- a/vendor/github.com/go-asn1-ber/asn1-ber/ber.go +++ b/vendor/github.com/go-asn1-ber/asn1-ber/ber.go @@ -170,12 +170,10 @@ func PrintPacket(p *Packet) { printPacket(os.Stdout, p, 0, false) } -func printPacket(out io.Writer, p *Packet, indent int, printBytes bool) { - indentStr := "" - - for len(indentStr) != indent { - indentStr += " " - } +// Return a string describing packet content. This is not recursive, +// If the packet is a sequence, use `printPacket()`, or browse +// sequence yourself. +func DescribePacket(p *Packet) string { classStr := ClassMap[p.ClassType] @@ -194,7 +192,17 @@ func printPacket(out io.Writer, p *Packet, indent int, printBytes bool) { description = p.Description + ": " } - _, _ = fmt.Fprintf(out, "%s%s(%s, %s, %s) Len=%d %q\n", indentStr, description, classStr, tagTypeStr, tagStr, p.Data.Len(), value) + return fmt.Sprintf("%s(%s, %s, %s) Len=%d %q", description, classStr, tagTypeStr, tagStr, p.Data.Len(), value) +} + +func printPacket(out io.Writer, p *Packet, indent int, printBytes bool) { + indentStr := "" + + for len(indentStr) != indent { + indentStr += " " + } + + _, _ = fmt.Fprintf(out, "%s%s\n", indentStr, DescribePacket(p)) if printBytes { PrintBytes(out, p.Bytes(), indentStr) @@ -317,7 +325,7 @@ func readPacket(reader io.Reader) (*Packet, int, error) { // Read the next packet child, r, err := readPacket(reader) if err != nil { - return nil, read, err + return nil, read, unexpectedEOF(err) } contentRead += r read += r @@ -348,10 +356,7 @@ func readPacket(reader io.Reader) (*Packet, int, error) { if length > 0 { _, err := io.ReadFull(reader, content) if err != nil { - if err == io.EOF { - return nil, read, io.ErrUnexpectedEOF - } - return nil, read, err + return nil, read, unexpectedEOF(err) } read += length } diff --git a/vendor/github.com/go-asn1-ber/asn1-ber/identifier.go b/vendor/github.com/go-asn1-ber/asn1-ber/identifier.go index e8c43574..c501d819 100644 --- a/vendor/github.com/go-asn1-ber/asn1-ber/identifier.go +++ b/vendor/github.com/go-asn1-ber/asn1-ber/identifier.go @@ -37,7 +37,7 @@ func readIdentifier(reader io.Reader) (Identifier, int, error) { if Debug { fmt.Printf("error reading high-tag-number tag byte %d: %v\n", tagBytes, err) } - return Identifier{}, read, err + return Identifier{}, read, unexpectedEOF(err) } tagBytes++ read++ diff --git a/vendor/github.com/go-asn1-ber/asn1-ber/length.go b/vendor/github.com/go-asn1-ber/asn1-ber/length.go index 9cc195d0..2c81cc3f 100644 --- a/vendor/github.com/go-asn1-ber/asn1-ber/length.go +++ b/vendor/github.com/go-asn1-ber/asn1-ber/length.go @@ -13,7 +13,7 @@ func readLength(reader io.Reader) (length int, read int, err error) { if Debug { fmt.Printf("error reading length byte: %v\n", err) } - return 0, 0, err + return 0, 0, unexpectedEOF(err) } read++ @@ -47,7 +47,7 @@ func readLength(reader io.Reader) (length int, read int, err error) { if Debug { fmt.Printf("error reading long-form length byte %d: %v\n", i, err) } - return 0, read, err + return 0, read, unexpectedEOF(err) } read++ diff --git a/vendor/github.com/go-asn1-ber/asn1-ber/real.go b/vendor/github.com/go-asn1-ber/asn1-ber/real.go index 610a003a..9f637a57 100644 --- a/vendor/github.com/go-asn1-ber/asn1-ber/real.go +++ b/vendor/github.com/go-asn1-ber/asn1-ber/real.go @@ -89,12 +89,18 @@ func parseBinaryFloat(v []byte) (float64, error) { case 0x02: expLen = 3 case 0x03: + if len(v) < 2 { + return 0.0, errors.New("invalid data") + } expLen = int(v[0]) if expLen > 8 { return 0.0, errors.New("too big value of exponent") } v = v[1:] } + if expLen > len(v) { + return 0.0, errors.New("too big value of exponent") + } buf, v = v[:expLen], v[expLen:] exponent, err := ParseInt64(buf) if err != nil { diff --git a/vendor/github.com/go-asn1-ber/asn1-ber/util.go b/vendor/github.com/go-asn1-ber/asn1-ber/util.go index 14dc87d7..da45e9fa 100644 --- a/vendor/github.com/go-asn1-ber/asn1-ber/util.go +++ b/vendor/github.com/go-asn1-ber/asn1-ber/util.go @@ -6,14 +6,18 @@ func readByte(reader io.Reader) (byte, error) { bytes := make([]byte, 1) _, err := io.ReadFull(reader, bytes) if err != nil { - if err == io.EOF { - return 0, io.ErrUnexpectedEOF - } return 0, err } return bytes[0], nil } +func unexpectedEOF(err error) error { + if err == io.EOF { + return io.ErrUnexpectedEOF + } + return err +} + func isEOCPacket(p *Packet) bool { return p != nil && p.Tag == TagEOC && diff --git a/vendor/github.com/go-ldap/ldap/v3/add.go b/vendor/github.com/go-ldap/ldap/v3/add.go index baecd787..ab32b0b6 100644 --- a/vendor/github.com/go-ldap/ldap/v3/add.go +++ b/vendor/github.com/go-ldap/ldap/v3/add.go @@ -1,8 +1,7 @@ package ldap import ( - "log" - + "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) @@ -63,7 +62,6 @@ func NewAddRequest(dn string, controls []Control) *AddRequest { DN: dn, Controls: controls, } - } // Add performs the given AddRequest @@ -85,7 +83,7 @@ func (l *Conn) Add(addRequest *AddRequest) error { return err } } else { - log.Printf("Unexpected Response: %d", packet.Children[1].Tag) + return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) } return nil } diff --git a/vendor/github.com/go-ldap/ldap/v3/bind.go b/vendor/github.com/go-ldap/ldap/v3/bind.go index 9bc57482..a37f8e2c 100644 --- a/vendor/github.com/go-ldap/ldap/v3/bind.go +++ b/vendor/github.com/go-ldap/ldap/v3/bind.go @@ -261,7 +261,7 @@ func parseParams(str string) (map[string]string, error) { var state int for i := 0; i <= len(str); i++ { switch state { - case 0: //reading key + case 0: // reading key if i == len(str) { return nil, fmt.Errorf("syntax error on %d", i) } @@ -270,7 +270,7 @@ func parseParams(str string) (map[string]string, error) { continue } state = 1 - case 1: //reading value + case 1: // reading value if i == len(str) { m[key] = value break @@ -289,7 +289,7 @@ func parseParams(str string) (map[string]string, error) { default: value += string(str[i]) } - case 2: //inside quotes + case 2: // inside quotes if i == len(str) { return nil, fmt.Errorf("syntax error on %d", i) } @@ -399,6 +399,9 @@ type NTLMBindRequest struct { Username string // Password is the credentials to bind with Password string + // AllowEmptyPassword sets whether the client allows binding with an empty password + // (normally used for unauthenticated bind). + AllowEmptyPassword bool // Hash is the hex NTLM hash to bind with. Password or hash must be provided Hash string // Controls are optional controls to send with the bind request @@ -442,6 +445,22 @@ func (l *Conn) NTLMBind(domain, username, password string) error { return err } +// NTLMUnauthenticatedBind performs an bind with an empty password. +// +// A username is required. The anonymous bind is not (yet) supported by the go-ntlmssp library (https://github.com/Azure/go-ntlmssp/blob/819c794454d067543bc61d29f61fef4b3c3df62c/authenticate_message.go#L87) +// +// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 part 3.2.5.1.2 +func (l *Conn) NTLMUnauthenticatedBind(domain, username string) error { + req := &NTLMBindRequest{ + Domain: domain, + Username: username, + Password: "", + AllowEmptyPassword: true, + } + _, err := l.NTLMChallengeBind(req) + return err +} + // NTLMBindWithHash performs an NTLM Bind with an NTLM hash instead of plaintext password (pass-the-hash) func (l *Conn) NTLMBindWithHash(domain, username, hash string) error { req := &NTLMBindRequest{ @@ -455,7 +474,7 @@ func (l *Conn) NTLMBindWithHash(domain, username, hash string) error { // NTLMChallengeBind performs the NTLMSSP bind operation defined in the given request func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindResult, error) { - if ntlmBindRequest.Password == "" && ntlmBindRequest.Hash == "" { + if !ntlmBindRequest.AllowEmptyPassword && ntlmBindRequest.Password == "" && ntlmBindRequest.Hash == "" { return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) } @@ -496,10 +515,11 @@ func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindRes var err error var responseMessage []byte // generate a response message to the challenge with the given Username/Password if password is provided - if ntlmBindRequest.Password != "" { - responseMessage, err = ntlmssp.ProcessChallenge(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Password) - } else if ntlmBindRequest.Hash != "" { + if ntlmBindRequest.Hash != "" { responseMessage, err = ntlmssp.ProcessChallengeWithHash(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Hash) + } else if ntlmBindRequest.Password != "" || ntlmBindRequest.AllowEmptyPassword { + _, _, domainNeeded := ntlmssp.GetDomain(ntlmBindRequest.Username) + responseMessage, err = ntlmssp.ProcessChallenge(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Password, domainNeeded) } else { err = fmt.Errorf("need a password or hash to generate reply") } @@ -538,3 +558,178 @@ func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindRes err = GetLDAPError(packet) return result, err } + +// GSSAPIClient interface is used as the client-side implementation for the +// GSSAPI SASL mechanism. +// Interface inspired by GSSAPIClient from golang.org/x/crypto/ssh +type GSSAPIClient interface { + // InitSecContext initiates the establishment of a security context for + // GSS-API between the client and server. + // Initially the token parameter should be specified as nil. + // The routine may return a outputToken which should be transferred to + // the server, where the server will present it to AcceptSecContext. + // If no token need be sent, InitSecContext will indicate this by setting + // needContinue to false. To complete the context + // establishment, one or more reply tokens may be required from the server; + // if so, InitSecContext will return a needContinue which is true. + // In this case, InitSecContext should be called again when the + // reply token is received from the server, passing the reply token + // to InitSecContext via the token parameters. + // See RFC 4752 section 3.1. + InitSecContext(target string, token []byte) (outputToken []byte, needContinue bool, err error) + // NegotiateSaslAuth performs the last step of the Sasl handshake. + // It takes a token, which, when unwrapped, describes the servers supported + // security layers (first octet) and maximum receive buffer (remaining + // three octets). + // If the received token is unacceptable an error must be returned to abort + // the handshake. + // Outputs a signed token describing the client's selected security layer + // and receive buffer size and optionally an authorization identity. + // The returned token will be sent to the server and the handshake considered + // completed successfully and the server authenticated. + // See RFC 4752 section 3.1. + NegotiateSaslAuth(token []byte, authzid string) ([]byte, error) + // DeleteSecContext destroys any established secure context. + DeleteSecContext() error +} + +// GSSAPIBindRequest represents a GSSAPI SASL mechanism bind request. +// See rfc4752 and rfc4513 section 5.2.1.2. +type GSSAPIBindRequest struct { + // Service Principal Name user for the service ticket. Eg. "ldap/" + ServicePrincipalName string + // (Optional) Authorization entity + AuthZID string + // (Optional) Controls to send with the bind request + Controls []Control +} + +// GSSAPIBind performs the GSSAPI SASL bind using the provided GSSAPI client. +func (l *Conn) GSSAPIBind(client GSSAPIClient, servicePrincipal, authzid string) error { + return l.GSSAPIBindRequest(client, &GSSAPIBindRequest{ + ServicePrincipalName: servicePrincipal, + AuthZID: authzid, + }) +} + +// GSSAPIBindRequest performs the GSSAPI SASL bind using the provided GSSAPI client. +func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest) error { + //nolint:errcheck + defer client.DeleteSecContext() + + var err error + var reqToken []byte + var recvToken []byte + needInit := true + for { + if needInit { + // Establish secure context between client and server. + reqToken, needInit, err = client.InitSecContext(req.ServicePrincipalName, recvToken) + if err != nil { + return err + } + } else { + // Secure context is set up, perform the last step of SASL handshake. + reqToken, err = client.NegotiateSaslAuth(recvToken, req.AuthZID) + if err != nil { + return err + } + } + // Send Bind request containing the current token and extract the + // token sent by server. + recvToken, err = l.saslBindTokenExchange(req.Controls, reqToken) + if err != nil { + return err + } + + if !needInit && len(recvToken) == 0 { + break + } + } + + return nil +} + +func (l *Conn) saslBindTokenExchange(reqControls []Control, reqToken []byte) ([]byte, error) { + // Construct LDAP Bind request with GSSAPI SASL mechanism. + envelope := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + envelope.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") + request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) + request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) + + auth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication") + auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "GSSAPI", "SASL Mech")) + if len(reqToken) > 0 { + auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, string(reqToken), "Credentials")) + } + request.AppendChild(auth) + envelope.AppendChild(request) + if len(reqControls) > 0 { + envelope.AppendChild(encodeControls(reqControls)) + } + + msgCtx, err := l.sendMessage(envelope) + if err != nil { + return nil, err + } + defer l.finishMessage(msgCtx) + + packet, err := l.readPacket(msgCtx) + if err != nil { + return nil, err + } + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if l.Debug { + if err = addLDAPDescriptions(packet); err != nil { + return nil, err + } + ber.PrintPacket(packet) + } + + // https://www.rfc-editor.org/rfc/rfc4511#section-4.1.1 + // packet is an envelope + // child 0 is message id + // child 1 is protocolOp + if len(packet.Children) != 2 { + return nil, fmt.Errorf("bad bind response") + } + + protocolOp := packet.Children[1] +RESP: + switch protocolOp.Description { + case "Bind Response": // Bind Response + // Bind Reponse is an LDAP Response (https://www.rfc-editor.org/rfc/rfc4511#section-4.1.9) + // with an additional optional serverSaslCreds string (https://www.rfc-editor.org/rfc/rfc4511#section-4.2.2) + // child 0 is resultCode + resultCode := protocolOp.Children[0] + if resultCode.Tag != ber.TagEnumerated { + break RESP + } + switch resultCode.Value.(int64) { + case 14: // Sasl bind in progress + if len(protocolOp.Children) < 3 { + break RESP + } + referral := protocolOp.Children[3] + switch referral.Description { + case "Referral": + if referral.ClassType != ber.ClassContext || referral.Tag != ber.TagObjectDescriptor { + break RESP + } + return ioutil.ReadAll(referral.Data) + } + // Optional: + //if len(protocolOp.Children) == 4 { + // serverSaslCreds := protocolOp.Children[4] + //} + case 0: // Success - Bind OK. + // SASL layer in effect (if any) (See https://www.rfc-editor.org/rfc/rfc4513#section-5.2.1.4) + // NOTE: SASL security layers are not supported currently. + return nil, nil + } + } + + return nil, GetLDAPError(packet) +} diff --git a/vendor/github.com/go-ldap/ldap/v3/client.go b/vendor/github.com/go-ldap/ldap/v3/client.go index 1fa4ad5a..ed96e840 100644 --- a/vendor/github.com/go-ldap/ldap/v3/client.go +++ b/vendor/github.com/go-ldap/ldap/v3/client.go @@ -1,6 +1,7 @@ package ldap import ( + "context" "crypto/tls" "time" ) @@ -9,14 +10,18 @@ import ( type Client interface { Start() StartTLS(*tls.Config) error - Close() + Close() error + GetLastError() error IsClosing() bool SetTimeout(time.Duration) + TLSConnectionState() (tls.ConnectionState, bool) Bind(username, password string) error UnauthenticatedBind(username string) error SimpleBind(*SimpleBindRequest) (*SimpleBindResult, error) ExternalBind() error + NTLMUnauthenticatedBind(domain, username string) error + Unbind() error Add(*AddRequest) error Del(*DelRequest) error @@ -28,5 +33,9 @@ type Client interface { PasswordModify(*PasswordModifyRequest) (*PasswordModifyResult, error) Search(*SearchRequest) (*SearchResult, error) + SearchAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) + DirSync(searchRequest *SearchRequest, flags, maxAttrCount int64, cookie []byte) (*SearchResult, error) + DirSyncAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int, flags, maxAttrCount int64, cookie []byte) Response + Syncrepl(ctx context.Context, searchRequest *SearchRequest, bufferSize int, mode ControlSyncRequestMode, cookie []byte, reloadHint bool) Response } diff --git a/vendor/github.com/go-ldap/ldap/v3/compare.go b/vendor/github.com/go-ldap/ldap/v3/compare.go index cd43e4c5..a1cd760b 100644 --- a/vendor/github.com/go-ldap/ldap/v3/compare.go +++ b/vendor/github.com/go-ldap/ldap/v3/compare.go @@ -34,7 +34,8 @@ func (l *Conn) Compare(dn, attribute, value string) (bool, error) { msgCtx, err := l.doRequest(&CompareRequest{ DN: dn, Attribute: attribute, - Value: value}) + Value: value, + }) if err != nil { return false, err } diff --git a/vendor/github.com/go-ldap/ldap/v3/conn.go b/vendor/github.com/go-ldap/ldap/v3/conn.go index ae5e19af..6d083621 100644 --- a/vendor/github.com/go-ldap/ldap/v3/conn.go +++ b/vendor/github.com/go-ldap/ldap/v3/conn.go @@ -2,10 +2,10 @@ package ldap import ( "bufio" + "context" "crypto/tls" "errors" "fmt" - "log" "net" "net/url" "sync" @@ -61,13 +61,21 @@ type messageContext struct { // sendResponse should only be called within the processMessages() loop which // is also responsible for closing the responses channel. -func (msgCtx *messageContext) sendResponse(packet *PacketResponse) { +func (msgCtx *messageContext) sendResponse(packet *PacketResponse, timeout time.Duration) { + timeoutCtx := context.Background() + if timeout > 0 { + var cancelFunc context.CancelFunc + timeoutCtx, cancelFunc = context.WithTimeout(context.Background(), timeout) + defer cancelFunc() + } select { case msgCtx.responses <- packet: // Successfully sent packet to message handler. case <-msgCtx.done: // The request handler is done and will not receive more // packets. + case <-timeoutCtx.Done(): + // The timeout was reached before the packet was sent. } } @@ -88,6 +96,7 @@ const ( type Conn struct { // requestTimeout is loaded atomically // so we need to ensure 64-bit alignment on 32-bit platforms. + // https://github.com/go-ldap/ldap/pull/199 requestTimeout int64 conn net.Conn isTLS bool @@ -102,6 +111,8 @@ type Conn struct { wgClose sync.WaitGroup outstandingRequests uint messageMutex sync.Mutex + + err error } var _ Client = &Conn{} @@ -119,30 +130,31 @@ type DialOpt func(*DialContext) // DialWithDialer updates net.Dialer in DialContext. func DialWithDialer(d *net.Dialer) DialOpt { return func(dc *DialContext) { - dc.d = d + dc.dialer = d } } // DialWithTLSConfig updates tls.Config in DialContext. func DialWithTLSConfig(tc *tls.Config) DialOpt { return func(dc *DialContext) { - dc.tc = tc + dc.tlsConfig = tc } } // DialWithTLSDialer is a wrapper for DialWithTLSConfig with the option to // specify a net.Dialer to for example define a timeout or a custom resolver. +// @deprecated Use DialWithDialer and DialWithTLSConfig instead func DialWithTLSDialer(tlsConfig *tls.Config, dialer *net.Dialer) DialOpt { return func(dc *DialContext) { - dc.tc = tlsConfig - dc.d = dialer + dc.tlsConfig = tlsConfig + dc.dialer = dialer } } // DialContext contains necessary parameters to dial the given ldap URL. type DialContext struct { - d *net.Dialer - tc *tls.Config + dialer *net.Dialer + tlsConfig *tls.Config } func (dc *DialContext) dial(u *url.URL) (net.Conn, error) { @@ -150,7 +162,7 @@ func (dc *DialContext) dial(u *url.URL) (net.Conn, error) { if u.Path == "" || u.Path == "/" { u.Path = "/var/run/slapd/ldapi" } - return dc.d.Dial("unix", u.Path) + return dc.dialer.Dial("unix", u.Path) } host, port, err := net.SplitHostPort(u.Host) @@ -161,16 +173,21 @@ func (dc *DialContext) dial(u *url.URL) (net.Conn, error) { } switch u.Scheme { + case "cldap": + if port == "" { + port = DefaultLdapPort + } + return dc.dialer.Dial("udp", net.JoinHostPort(host, port)) case "ldap": if port == "" { port = DefaultLdapPort } - return dc.d.Dial("tcp", net.JoinHostPort(host, port)) + return dc.dialer.Dial("tcp", net.JoinHostPort(host, port)) case "ldaps": if port == "" { port = DefaultLdapsPort } - return tls.DialWithDialer(dc.d, "tcp", net.JoinHostPort(host, port), dc.tc) + return tls.DialWithDialer(dc.dialer, "tcp", net.JoinHostPort(host, port), dc.tlsConfig) } return nil, fmt.Errorf("Unknown scheme '%s'", u.Scheme) @@ -203,7 +220,8 @@ func DialTLS(network, addr string, config *tls.Config) (*Conn, error) { } // DialURL connects to the given ldap URL. -// The following schemas are supported: ldap://, ldaps://, ldapi://. +// The following schemas are supported: ldap://, ldaps://, ldapi://, +// and cldap:// (RFC1798, deprecated but used by Active Directory). // On success a new Conn for the connection is returned. func DialURL(addr string, opts ...DialOpt) (*Conn, error) { u, err := url.Parse(addr) @@ -215,8 +233,8 @@ func DialURL(addr string, opts ...DialOpt) (*Conn, error) { for _, opt := range opts { opt(&dc) } - if dc.d == nil { - dc.d = &net.Dialer{Timeout: DefaultTimeout} + if dc.dialer == nil { + dc.dialer = &net.Dialer{Timeout: DefaultTimeout} } c, err := dc.dial(u) @@ -231,7 +249,7 @@ func DialURL(addr string, opts ...DialOpt) (*Conn, error) { // NewConn returns a new Conn using conn for network I/O. func NewConn(conn net.Conn, isTLS bool) *Conn { - return &Conn{ + l := &Conn{ conn: conn, chanConfirm: make(chan struct{}), chanMessageID: make(chan int64), @@ -240,11 +258,12 @@ func NewConn(conn net.Conn, isTLS bool) *Conn { requestTimeout: 0, isTLS: isTLS, } + l.wgClose.Add(1) + return l } // Start initializes goroutines to read responses and process messages func (l *Conn) Start() { - l.wgClose.Add(1) go l.reader() go l.processMessages() } @@ -260,31 +279,45 @@ func (l *Conn) setClosing() bool { } // Close closes the connection. -func (l *Conn) Close() { +func (l *Conn) Close() (err error) { l.messageMutex.Lock() defer l.messageMutex.Unlock() if l.setClosing() { l.Debug.Printf("Sending quit message and waiting for confirmation") l.chanMessage <- &messagePacket{Op: MessageQuit} - <-l.chanConfirm + + timeoutCtx := context.Background() + if l.getTimeout() > 0 { + var cancelFunc context.CancelFunc + timeoutCtx, cancelFunc = context.WithTimeout(timeoutCtx, time.Duration(l.getTimeout())) + defer cancelFunc() + } + select { + case <-l.chanConfirm: + // Confirmation was received. + case <-timeoutCtx.Done(): + // The timeout was reached before confirmation was received. + } + close(l.chanMessage) l.Debug.Printf("Closing network connection") - if err := l.conn.Close(); err != nil { - log.Println(err) - } - + err = l.conn.Close() l.wgClose.Done() } l.wgClose.Wait() + + return err } // SetTimeout sets the time after a request is sent that a MessageTimeout triggers func (l *Conn) SetTimeout(timeout time.Duration) { - if timeout > 0 { - atomic.StoreInt64(&l.requestTimeout, int64(timeout)) - } + atomic.StoreInt64(&l.requestTimeout, int64(timeout)) +} + +func (l *Conn) getTimeout() int64 { + return atomic.LoadInt64(&l.requestTimeout) } // Returns the next available messageID @@ -295,6 +328,14 @@ func (l *Conn) nextMessageID() int64 { return 0 } +// GetLastError returns the last recorded error from goroutines like processMessages and reader. +// Only the last recorded error will be returned. +func (l *Conn) GetLastError() error { + l.messageMutex.Lock() + defer l.messageMutex.Unlock() + return l.err +} + // StartTLS sends the command to start a TLS session and then creates a new TLS Client func (l *Conn) StartTLS(config *tls.Config) error { if l.isTLS { @@ -443,13 +484,13 @@ func (l *Conn) sendProcessMessage(message *messagePacket) bool { func (l *Conn) processMessages() { defer func() { if err := recover(); err != nil { - log.Printf("ldap: recovered panic in processMessages: %v", err) + l.err = fmt.Errorf("ldap: recovered panic in processMessages: %v", err) } for messageID, msgCtx := range l.messageContexts { // If we are closing due to an error, inform anyone who // is waiting about the error. if l.IsClosing() && l.closeErr.Load() != nil { - msgCtx.sendResponse(&PacketResponse{Error: l.closeErr.Load().(error)}) + msgCtx.sendResponse(&PacketResponse{Error: l.closeErr.Load().(error)}, time.Duration(l.getTimeout())) } l.Debug.Printf("Closing channel for MessageID %d", messageID) close(msgCtx.responses) @@ -477,7 +518,7 @@ func (l *Conn) processMessages() { _, err := l.conn.Write(buf) if err != nil { l.Debug.Printf("Error Sending Message: %s", err.Error()) - message.Context.sendResponse(&PacketResponse{Error: fmt.Errorf("unable to send request: %s", err)}) + message.Context.sendResponse(&PacketResponse{Error: fmt.Errorf("unable to send request: %s", err)}, time.Duration(l.getTimeout())) close(message.Context.responses) break } @@ -487,28 +528,35 @@ func (l *Conn) processMessages() { l.messageContexts[message.MessageID] = message.Context // Add timeout if defined - requestTimeout := time.Duration(atomic.LoadInt64(&l.requestTimeout)) + requestTimeout := l.getTimeout() if requestTimeout > 0 { go func() { + timer := time.NewTimer(time.Duration(requestTimeout)) defer func() { if err := recover(); err != nil { - log.Printf("ldap: recovered panic in RequestTimeout: %v", err) + l.err = fmt.Errorf("ldap: recovered panic in RequestTimeout: %v", err) } + + timer.Stop() }() - time.Sleep(requestTimeout) - timeoutMessage := &messagePacket{ - Op: MessageTimeout, - MessageID: message.MessageID, + + select { + case <-timer.C: + timeoutMessage := &messagePacket{ + Op: MessageTimeout, + MessageID: message.MessageID, + } + l.sendProcessMessage(timeoutMessage) + case <-message.Context.done: } - l.sendProcessMessage(timeoutMessage) }() } case MessageResponse: l.Debug.Printf("Receiving message %d", message.MessageID) if msgCtx, ok := l.messageContexts[message.MessageID]; ok { - msgCtx.sendResponse(&PacketResponse{message.Packet, nil}) + msgCtx.sendResponse(&PacketResponse{message.Packet, nil}, time.Duration(l.getTimeout())) } else { - log.Printf("Received unexpected message %d, %v", message.MessageID, l.IsClosing()) + l.err = fmt.Errorf("ldap: received unexpected message %d, %v", message.MessageID, l.IsClosing()) l.Debug.PrintPacket(message.Packet) } case MessageTimeout: @@ -516,7 +564,7 @@ func (l *Conn) processMessages() { // All reads will return immediately if msgCtx, ok := l.messageContexts[message.MessageID]; ok { l.Debug.Printf("Receiving message timeout for %d", message.MessageID) - msgCtx.sendResponse(&PacketResponse{message.Packet, NewError(ErrorNetwork, errors.New("ldap: connection timed out"))}) + msgCtx.sendResponse(&PacketResponse{message.Packet, NewError(ErrorNetwork, errors.New("ldap: connection timed out"))}, time.Duration(l.getTimeout())) delete(l.messageContexts, message.MessageID) close(msgCtx.responses) } @@ -535,7 +583,7 @@ func (l *Conn) reader() { cleanstop := false defer func() { if err := recover(); err != nil { - log.Printf("ldap: recovered panic in reader: %v", err) + l.err = fmt.Errorf("ldap: recovered panic in reader: %v", err) } if !cleanstop { l.Close() diff --git a/vendor/github.com/go-ldap/ldap/v3/control.go b/vendor/github.com/go-ldap/ldap/v3/control.go index 64fb002a..60453deb 100644 --- a/vendor/github.com/go-ldap/ldap/v3/control.go +++ b/vendor/github.com/go-ldap/ldap/v3/control.go @@ -5,6 +5,7 @@ import ( "strconv" ber "github.com/go-asn1-ber/asn1-ber" + "github.com/google/uuid" ) const ( @@ -20,6 +21,13 @@ const ( ControlTypeManageDsaIT = "2.16.840.1.113730.3.4.2" // ControlTypeWhoAmI - https://tools.ietf.org/html/rfc4532 ControlTypeWhoAmI = "1.3.6.1.4.1.4203.1.11.3" + // ControlTypeSubtreeDelete - https://datatracker.ietf.org/doc/html/draft-armijo-ldap-treedelete-02 + ControlTypeSubtreeDelete = "1.2.840.113556.1.4.805" + + // ControlTypeServerSideSorting - https://www.ietf.org/rfc/rfc2891.txt + ControlTypeServerSideSorting = "1.2.840.113556.1.4.473" + // ControlTypeServerSideSorting - https://www.ietf.org/rfc/rfc2891.txt + ControlTypeServerSideSortingResult = "1.2.840.113556.1.4.474" // ControlTypeMicrosoftNotification - https://msdn.microsoft.com/en-us/library/aa366983(v=vs.85).aspx ControlTypeMicrosoftNotification = "1.2.840.113556.1.4.528" @@ -27,16 +35,43 @@ const ( ControlTypeMicrosoftShowDeleted = "1.2.840.113556.1.4.417" // ControlTypeMicrosoftServerLinkTTL - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/f4f523a8-abc0-4b3a-a471-6b2fef135481?redirectedfrom=MSDN ControlTypeMicrosoftServerLinkTTL = "1.2.840.113556.1.4.2309" + // ControlTypeDirSync - Active Directory DirSync - https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx + ControlTypeDirSync = "1.2.840.113556.1.4.841" + + // ControlTypeSyncRequest - https://www.ietf.org/rfc/rfc4533.txt + ControlTypeSyncRequest = "1.3.6.1.4.1.4203.1.9.1.1" + // ControlTypeSyncState - https://www.ietf.org/rfc/rfc4533.txt + ControlTypeSyncState = "1.3.6.1.4.1.4203.1.9.1.2" + // ControlTypeSyncDone - https://www.ietf.org/rfc/rfc4533.txt + ControlTypeSyncDone = "1.3.6.1.4.1.4203.1.9.1.3" + // ControlTypeSyncInfo - https://www.ietf.org/rfc/rfc4533.txt + ControlTypeSyncInfo = "1.3.6.1.4.1.4203.1.9.1.4" +) + +// Flags for DirSync control +const ( + DirSyncIncrementalValues int64 = 2147483648 + DirSyncPublicDataOnly int64 = 8192 + DirSyncAncestorsFirstOrder int64 = 2048 + DirSyncObjectSecurity int64 = 1 ) // ControlTypeMap maps controls to text descriptions var ControlTypeMap = map[string]string{ - ControlTypePaging: "Paging", - ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft", - ControlTypeManageDsaIT: "Manage DSA IT", - ControlTypeMicrosoftNotification: "Change Notification - Microsoft", - ControlTypeMicrosoftShowDeleted: "Show Deleted Objects - Microsoft", - ControlTypeMicrosoftServerLinkTTL: "Return TTL-DNs for link values with associated expiry times - Microsoft", + ControlTypePaging: "Paging", + ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft", + ControlTypeManageDsaIT: "Manage DSA IT", + ControlTypeSubtreeDelete: "Subtree Delete Control", + ControlTypeMicrosoftNotification: "Change Notification - Microsoft", + ControlTypeMicrosoftShowDeleted: "Show Deleted Objects - Microsoft", + ControlTypeMicrosoftServerLinkTTL: "Return TTL-DNs for link values with associated expiry times - Microsoft", + ControlTypeServerSideSorting: "Server Side Sorting Request - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)", + ControlTypeServerSideSortingResult: "Server Side Sorting Results - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)", + ControlTypeDirSync: "DirSync", + ControlTypeSyncRequest: "Sync Request", + ControlTypeSyncState: "Sync State", + ControlTypeSyncDone: "Sync Done", + ControlTypeSyncInfo: "Sync Info", } // Control defines an interface controls provide to encode and describe themselves @@ -229,7 +264,7 @@ func (c *ControlManageDsaIT) GetControlType() string { // Encode returns the ber packet representation func (c *ControlManageDsaIT) Encode() *ber.Packet { - //FIXME + // FIXME packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeManageDsaIT, "Control Type ("+ControlTypeMap[ControlTypeManageDsaIT]+")")) if c.Criticality { @@ -369,7 +404,13 @@ func DecodeControl(packet *ber.Packet) (Control, error) { case 2: packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" - ControlType = packet.Children[0].Value.(string) + if packet.Children[0].Value != nil { + ControlType = packet.Children[0].Value.(string) + } else if packet.Children[0].Data != nil { + ControlType = packet.Children[0].Data.String() + } else { + return nil, fmt.Errorf("not found where to get the control type") + } // Children[1] could be criticality or value (both are optional) // duck-type on whether this is a boolean @@ -436,18 +477,18 @@ func DecodeControl(packet *ber.Packet) (Control, error) { for _, child := range sequence.Children { if child.Tag == 0 { - //Warning + // Warning warningPacket := child.Children[0] val, err := ber.ParseInt64(warningPacket.Data.Bytes()) if err != nil { return nil, fmt.Errorf("failed to decode data bytes: %s", err) } if warningPacket.Tag == 0 { - //timeBeforeExpiration + // timeBeforeExpiration c.Expire = val warningPacket.Value = c.Expire } else if warningPacket.Tag == 1 { - //graceAuthNsRemaining + // graceAuthNsRemaining c.Grace = val warningPacket.Value = c.Grace } @@ -485,6 +526,36 @@ func DecodeControl(packet *ber.Packet) (Control, error) { return NewControlMicrosoftShowDeleted(), nil case ControlTypeMicrosoftServerLinkTTL: return NewControlMicrosoftServerLinkTTL(), nil + case ControlTypeSubtreeDelete: + return NewControlSubtreeDelete(), nil + case ControlTypeServerSideSorting: + return NewControlServerSideSorting(value) + case ControlTypeServerSideSortingResult: + return NewControlServerSideSortingResult(value) + case ControlTypeDirSync: + value.Description += " (DirSync)" + return NewResponseControlDirSync(value) + case ControlTypeSyncState: + value.Description += " (Sync State)" + valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to decode data bytes: %s", err) + } + return NewControlSyncState(valueChildren) + case ControlTypeSyncDone: + value.Description += " (Sync Done)" + valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to decode data bytes: %s", err) + } + return NewControlSyncDone(valueChildren) + case ControlTypeSyncInfo: + value.Description += " (Sync Info)" + valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to decode data bytes: %s", err) + } + return NewControlSyncInfo(valueChildren) default: c := new(ControlString) c.ControlType = ControlType @@ -519,6 +590,35 @@ func NewControlBeheraPasswordPolicy() *ControlBeheraPasswordPolicy { } } +// ControlSubtreeDelete implements the subtree delete control described in +// https://datatracker.ietf.org/doc/html/draft-armijo-ldap-treedelete-02 +type ControlSubtreeDelete struct{} + +// GetControlType returns the OID +func (c *ControlSubtreeDelete) GetControlType() string { + return ControlTypeSubtreeDelete +} + +// NewControlSubtreeDelete returns a ControlSubtreeDelete control. +func NewControlSubtreeDelete() *ControlSubtreeDelete { + return &ControlSubtreeDelete{} +} + +// Encode returns the ber packet representation +func (c *ControlSubtreeDelete) Encode() *ber.Packet { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeSubtreeDelete, "Control Type ("+ControlTypeMap[ControlTypeSubtreeDelete]+")")) + + return packet +} + +func (c *ControlSubtreeDelete) String() string { + return fmt.Sprintf( + "Control Type: %s (%q)", + ControlTypeMap[ControlTypeSubtreeDelete], + ControlTypeSubtreeDelete) +} + func encodeControls(controls []Control) *ber.Packet { packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0, nil, "Controls") for _, control := range controls { @@ -526,3 +626,669 @@ func encodeControls(controls []Control) *ber.Packet { } return packet } + +// ControlDirSync implements the control described in https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx +type ControlDirSync struct { + Criticality bool + Flags int64 + MaxAttrCount int64 + Cookie []byte +} + +// @deprecated Use NewRequestControlDirSync instead +func NewControlDirSync(flags int64, maxAttrCount int64, cookie []byte) *ControlDirSync { + return NewRequestControlDirSync(flags, maxAttrCount, cookie) +} + +// NewRequestControlDirSync returns a dir sync control +func NewRequestControlDirSync( + flags int64, maxAttrCount int64, cookie []byte, +) *ControlDirSync { + return &ControlDirSync{ + Criticality: true, + Flags: flags, + MaxAttrCount: maxAttrCount, + Cookie: cookie, + } +} + +// NewResponseControlDirSync returns a dir sync control +func NewResponseControlDirSync(value *ber.Packet) (*ControlDirSync, error) { + if value.Value != nil { + valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to decode data bytes: %s", err) + } + value.Data.Truncate(0) + value.Value = nil + value.AppendChild(valueChildren) + } + child := value.Children[0] + if len(child.Children) != 3 { // also on initial creation, Cookie is an empty string + return nil, fmt.Errorf("invalid number of children in dirSync control") + } + child.Description = "DirSync Control Value" + child.Children[0].Description = "Flags" + child.Children[1].Description = "MaxAttrCount" + child.Children[2].Description = "Cookie" + + cookie := child.Children[2].Data.Bytes() + child.Children[2].Value = cookie + return &ControlDirSync{ + Criticality: true, + Flags: child.Children[0].Value.(int64), + MaxAttrCount: child.Children[1].Value.(int64), + Cookie: cookie, + }, nil +} + +// GetControlType returns the OID +func (c *ControlDirSync) GetControlType() string { + return ControlTypeDirSync +} + +// String returns a human-readable description +func (c *ControlDirSync) String() string { + return fmt.Sprintf( + "ControlType: %s (%q) Criticality: %t ControlValue: Flags: %d MaxAttrCount: %d", + ControlTypeMap[ControlTypeDirSync], + ControlTypeDirSync, + c.Criticality, + c.Flags, + c.MaxAttrCount, + ) +} + +// Encode returns the ber packet representation +func (c *ControlDirSync) Encode() *ber.Packet { + cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "Cookie") + if len(c.Cookie) != 0 { + cookie.Value = c.Cookie + cookie.Data.Write(c.Cookie) + } + + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeDirSync, "Control Type ("+ControlTypeMap[ControlTypeDirSync]+")")) + packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) // must be true always + + val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (DirSync)") + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "DirSync Control Value") + seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.Flags), "Flags")) + seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.MaxAttrCount), "MaxAttrCount")) + seq.AppendChild(cookie) + val.AppendChild(seq) + + packet.AppendChild(val) + return packet +} + +// SetCookie stores the given cookie in the dirSync control +func (c *ControlDirSync) SetCookie(cookie []byte) { + c.Cookie = cookie +} + +// ControlServerSideSorting + +type SortKey struct { + Reverse bool + AttributeType string + MatchingRule string +} + +type ControlServerSideSorting struct { + SortKeys []*SortKey +} + +func (c *ControlServerSideSorting) GetControlType() string { + return ControlTypeServerSideSorting +} + +func NewControlServerSideSorting(value *ber.Packet) (*ControlServerSideSorting, error) { + sortKeys := []*SortKey{} + + val := value.Children[1].Children + + if len(val) != 1 { + return nil, fmt.Errorf("no sequence value in packet") + } + + sequences := val[0].Children + + for i, sequence := range sequences { + sortKey := &SortKey{} + + if len(sequence.Children) < 2 { + return nil, fmt.Errorf("attributeType or matchingRule is missing from sequence %d", i) + } + + sortKey.AttributeType = sequence.Children[0].Value.(string) + sortKey.MatchingRule = sequence.Children[1].Value.(string) + + if len(sequence.Children) == 3 { + sortKey.Reverse = sequence.Children[2].Value.(bool) + } + + sortKeys = append(sortKeys, sortKey) + } + + return &ControlServerSideSorting{SortKeys: sortKeys}, nil +} + +func NewControlServerSideSortingWithSortKeys(sortKeys []*SortKey) *ControlServerSideSorting { + return &ControlServerSideSorting{SortKeys: sortKeys} +} + +func (c *ControlServerSideSorting) Encode() *ber.Packet { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + control := ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.GetControlType(), "Control Type") + + value := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value") + seqs := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "SortKeyList") + + for _, f := range c.SortKeys { + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "") + + seq.AppendChild( + ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, f.AttributeType, "attributeType"), + ) + seq.AppendChild( + ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, f.MatchingRule, "orderingRule"), + ) + if f.Reverse { + seq.AppendChild( + ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, 1, f.Reverse, "reverseOrder"), + ) + } + + seqs.AppendChild(seq) + } + + value.AppendChild(seqs) + + packet.AppendChild(control) + packet.AppendChild(value) + + return packet +} + +func (c *ControlServerSideSorting) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality:%t %+v", + "Server Side Sorting", + c.GetControlType(), + false, + c.SortKeys, + ) +} + +// ControlServerSideSortingResponse + +const ( + ControlServerSideSortingCodeSuccess ControlServerSideSortingCode = 0 + ControlServerSideSortingCodeOperationsError ControlServerSideSortingCode = 1 + ControlServerSideSortingCodeTimeLimitExceeded ControlServerSideSortingCode = 2 + ControlServerSideSortingCodeStrongAuthRequired ControlServerSideSortingCode = 8 + ControlServerSideSortingCodeAdminLimitExceeded ControlServerSideSortingCode = 11 + ControlServerSideSortingCodeNoSuchAttribute ControlServerSideSortingCode = 16 + ControlServerSideSortingCodeInappropriateMatching ControlServerSideSortingCode = 18 + ControlServerSideSortingCodeInsufficientAccessRights ControlServerSideSortingCode = 50 + ControlServerSideSortingCodeBusy ControlServerSideSortingCode = 51 + ControlServerSideSortingCodeUnwillingToPerform ControlServerSideSortingCode = 53 + ControlServerSideSortingCodeOther ControlServerSideSortingCode = 80 +) + +var ControlServerSideSortingCodes = []ControlServerSideSortingCode{ + ControlServerSideSortingCodeSuccess, + ControlServerSideSortingCodeOperationsError, + ControlServerSideSortingCodeTimeLimitExceeded, + ControlServerSideSortingCodeStrongAuthRequired, + ControlServerSideSortingCodeAdminLimitExceeded, + ControlServerSideSortingCodeNoSuchAttribute, + ControlServerSideSortingCodeInappropriateMatching, + ControlServerSideSortingCodeInsufficientAccessRights, + ControlServerSideSortingCodeBusy, + ControlServerSideSortingCodeUnwillingToPerform, + ControlServerSideSortingCodeOther, +} + +type ControlServerSideSortingCode int64 + +// Valid test the code contained in the control against the ControlServerSideSortingCodes slice and return an error if the code is unknown. +func (c ControlServerSideSortingCode) Valid() error { + for _, validRet := range ControlServerSideSortingCodes { + if c == validRet { + return nil + } + } + return fmt.Errorf("unknown return code : %d", c) +} + +func NewControlServerSideSortingResult(pkt *ber.Packet) (*ControlServerSideSortingResult, error) { + control := &ControlServerSideSortingResult{} + + if pkt == nil || len(pkt.Children) == 0 { + return nil, fmt.Errorf("bad packet") + } + + codeInt, err := ber.ParseInt64(pkt.Children[0].Data.Bytes()) + if err != nil { + return nil, err + } + + code := ControlServerSideSortingCode(codeInt) + if err := code.Valid(); err != nil { + return nil, err + } + + return control, nil +} + +type ControlServerSideSortingResult struct { + Criticality bool + + Result ControlServerSideSortingCode + + // Not populated for now. I can't get openldap to send me this value, so I think this is specific to other directory server + // AttributeType string +} + +func (control *ControlServerSideSortingResult) GetControlType() string { + return ControlTypeServerSideSortingResult +} + +func (c *ControlServerSideSortingResult) Encode() *ber.Packet { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "SortResult sequence") + sortResult := ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, int64(c.Result), "SortResult") + packet.AppendChild(sortResult) + + return packet +} + +func (c *ControlServerSideSortingResult) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality:%t ResultCode:%+v", + "Server Side Sorting Result", + c.GetControlType(), + c.Criticality, + c.Result, + ) +} + +// Mode for ControlTypeSyncRequest +type ControlSyncRequestMode int64 + +const ( + SyncRequestModeRefreshOnly ControlSyncRequestMode = 1 + SyncRequestModeRefreshAndPersist ControlSyncRequestMode = 3 +) + +// ControlSyncRequest implements the Sync Request Control described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncRequest struct { + Criticality bool + Mode ControlSyncRequestMode + Cookie []byte + ReloadHint bool +} + +func NewControlSyncRequest( + mode ControlSyncRequestMode, cookie []byte, reloadHint bool, +) *ControlSyncRequest { + return &ControlSyncRequest{ + Criticality: true, + Mode: mode, + Cookie: cookie, + ReloadHint: reloadHint, + } +} + +// GetControlType returns the OID +func (c *ControlSyncRequest) GetControlType() string { + return ControlTypeSyncRequest +} + +// Encode encodes the control +func (c *ControlSyncRequest) Encode() *ber.Packet { + _mode := int64(c.Mode) + mode := ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, _mode, "Mode") + var cookie *ber.Packet + if len(c.Cookie) > 0 { + cookie = ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie") + cookie.Value = c.Cookie + cookie.Data.Write(c.Cookie) + } + reloadHint := ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.ReloadHint, "Reload Hint") + + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeSyncRequest, "Control Type ("+ControlTypeMap[ControlTypeSyncRequest]+")")) + packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) + + val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Sync Request)") + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Sync Request Value") + seq.AppendChild(mode) + if cookie != nil { + seq.AppendChild(cookie) + } + seq.AppendChild(reloadHint) + val.AppendChild(seq) + + packet.AppendChild(val) + return packet +} + +// String returns a human-readable description +func (c *ControlSyncRequest) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t Mode: %d Cookie: %s ReloadHint: %t", + ControlTypeMap[ControlTypeSyncRequest], + ControlTypeSyncRequest, + c.Criticality, + c.Mode, + string(c.Cookie), + c.ReloadHint, + ) +} + +// State for ControlSyncState +type ControlSyncStateState int64 + +const ( + SyncStatePresent ControlSyncStateState = 0 + SyncStateAdd ControlSyncStateState = 1 + SyncStateModify ControlSyncStateState = 2 + SyncStateDelete ControlSyncStateState = 3 +) + +// ControlSyncState implements the Sync State Control described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncState struct { + Criticality bool + State ControlSyncStateState + EntryUUID uuid.UUID + Cookie []byte +} + +func NewControlSyncState(pkt *ber.Packet) (*ControlSyncState, error) { + var ( + state ControlSyncStateState + entryUUID uuid.UUID + cookie []byte + err error + ) + switch len(pkt.Children) { + case 0, 1: + return nil, fmt.Errorf("at least two children are required: %d", len(pkt.Children)) + case 2: + state = ControlSyncStateState(pkt.Children[0].Value.(int64)) + entryUUID, err = uuid.FromBytes(pkt.Children[1].ByteValue) + if err != nil { + return nil, fmt.Errorf("failed to decode uuid: %w", err) + } + case 3: + state = ControlSyncStateState(pkt.Children[0].Value.(int64)) + entryUUID, err = uuid.FromBytes(pkt.Children[1].ByteValue) + if err != nil { + return nil, fmt.Errorf("failed to decode uuid: %w", err) + } + cookie = pkt.Children[2].ByteValue + } + return &ControlSyncState{ + Criticality: false, + State: state, + EntryUUID: entryUUID, + Cookie: cookie, + }, nil +} + +// GetControlType returns the OID +func (c *ControlSyncState) GetControlType() string { + return ControlTypeSyncState +} + +// Encode encodes the control +func (c *ControlSyncState) Encode() *ber.Packet { + return nil +} + +// String returns a human-readable description +func (c *ControlSyncState) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t State: %d EntryUUID: %s Cookie: %s", + ControlTypeMap[ControlTypeSyncState], + ControlTypeSyncState, + c.Criticality, + c.State, + c.EntryUUID.String(), + string(c.Cookie), + ) +} + +// ControlSyncDone implements the Sync Done Control described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncDone struct { + Criticality bool + Cookie []byte + RefreshDeletes bool +} + +func NewControlSyncDone(pkt *ber.Packet) (*ControlSyncDone, error) { + var ( + cookie []byte + refreshDeletes bool + ) + switch len(pkt.Children) { + case 0: + // have nothing to do + case 1: + cookie = pkt.Children[0].ByteValue + case 2: + cookie = pkt.Children[0].ByteValue + refreshDeletes = pkt.Children[1].Value.(bool) + } + return &ControlSyncDone{ + Criticality: false, + Cookie: cookie, + RefreshDeletes: refreshDeletes, + }, nil +} + +// GetControlType returns the OID +func (c *ControlSyncDone) GetControlType() string { + return ControlTypeSyncDone +} + +// Encode encodes the control +func (c *ControlSyncDone) Encode() *ber.Packet { + return nil +} + +// String returns a human-readable description +func (c *ControlSyncDone) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t Cookie: %s RefreshDeletes: %t", + ControlTypeMap[ControlTypeSyncDone], + ControlTypeSyncDone, + c.Criticality, + string(c.Cookie), + c.RefreshDeletes, + ) +} + +// Tag For ControlSyncInfo +type ControlSyncInfoValue uint64 + +const ( + SyncInfoNewcookie ControlSyncInfoValue = 0 + SyncInfoRefreshDelete ControlSyncInfoValue = 1 + SyncInfoRefreshPresent ControlSyncInfoValue = 2 + SyncInfoSyncIdSet ControlSyncInfoValue = 3 +) + +// ControlSyncInfoNewCookie implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfoNewCookie struct { + Cookie []byte +} + +// String returns a human-readable description +func (c *ControlSyncInfoNewCookie) String() string { + return fmt.Sprintf( + "NewCookie[Cookie: %s]", + string(c.Cookie), + ) +} + +// ControlSyncInfoRefreshDelete implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfoRefreshDelete struct { + Cookie []byte + RefreshDone bool +} + +// String returns a human-readable description +func (c *ControlSyncInfoRefreshDelete) String() string { + return fmt.Sprintf( + "RefreshDelete[Cookie: %s RefreshDone: %t]", + string(c.Cookie), + c.RefreshDone, + ) +} + +// ControlSyncInfoRefreshPresent implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfoRefreshPresent struct { + Cookie []byte + RefreshDone bool +} + +// String returns a human-readable description +func (c *ControlSyncInfoRefreshPresent) String() string { + return fmt.Sprintf( + "RefreshPresent[Cookie: %s RefreshDone: %t]", + string(c.Cookie), + c.RefreshDone, + ) +} + +// ControlSyncInfoSyncIdSet implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfoSyncIdSet struct { + Cookie []byte + RefreshDeletes bool + SyncUUIDs []uuid.UUID +} + +// String returns a human-readable description +func (c *ControlSyncInfoSyncIdSet) String() string { + return fmt.Sprintf( + "SyncIdSet[Cookie: %s RefreshDeletes: %t SyncUUIDs: %v]", + string(c.Cookie), + c.RefreshDeletes, + c.SyncUUIDs, + ) +} + +// ControlSyncInfo implements the Sync Info Control described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfo struct { + Criticality bool + Value ControlSyncInfoValue + NewCookie *ControlSyncInfoNewCookie + RefreshDelete *ControlSyncInfoRefreshDelete + RefreshPresent *ControlSyncInfoRefreshPresent + SyncIdSet *ControlSyncInfoSyncIdSet +} + +func NewControlSyncInfo(pkt *ber.Packet) (*ControlSyncInfo, error) { + var ( + cookie []byte + refreshDone = true + refreshDeletes bool + syncUUIDs []uuid.UUID + ) + c := &ControlSyncInfo{Criticality: false} + switch ControlSyncInfoValue(pkt.Identifier.Tag) { + case SyncInfoNewcookie: + c.Value = SyncInfoNewcookie + c.NewCookie = &ControlSyncInfoNewCookie{ + Cookie: pkt.ByteValue, + } + case SyncInfoRefreshDelete: + c.Value = SyncInfoRefreshDelete + switch len(pkt.Children) { + case 0: + // have nothing to do + case 1: + cookie = pkt.Children[0].ByteValue + case 2: + cookie = pkt.Children[0].ByteValue + refreshDone = pkt.Children[1].Value.(bool) + } + c.RefreshDelete = &ControlSyncInfoRefreshDelete{ + Cookie: cookie, + RefreshDone: refreshDone, + } + case SyncInfoRefreshPresent: + c.Value = SyncInfoRefreshPresent + switch len(pkt.Children) { + case 0: + // have nothing to do + case 1: + cookie = pkt.Children[0].ByteValue + case 2: + cookie = pkt.Children[0].ByteValue + refreshDone = pkt.Children[1].Value.(bool) + } + c.RefreshPresent = &ControlSyncInfoRefreshPresent{ + Cookie: cookie, + RefreshDone: refreshDone, + } + case SyncInfoSyncIdSet: + c.Value = SyncInfoSyncIdSet + switch len(pkt.Children) { + case 0: + // have nothing to do + case 1: + cookie = pkt.Children[0].ByteValue + case 2: + cookie = pkt.Children[0].ByteValue + refreshDeletes = pkt.Children[1].Value.(bool) + case 3: + cookie = pkt.Children[0].ByteValue + refreshDeletes = pkt.Children[1].Value.(bool) + syncUUIDs = make([]uuid.UUID, 0, len(pkt.Children[2].Children)) + for _, child := range pkt.Children[2].Children { + u, err := uuid.FromBytes(child.ByteValue) + if err != nil { + return nil, fmt.Errorf("failed to decode uuid: %w", err) + } + syncUUIDs = append(syncUUIDs, u) + } + } + c.SyncIdSet = &ControlSyncInfoSyncIdSet{ + Cookie: cookie, + RefreshDeletes: refreshDeletes, + SyncUUIDs: syncUUIDs, + } + default: + return nil, fmt.Errorf("unknown sync info value: %d", pkt.Identifier.Tag) + } + return c, nil +} + +// GetControlType returns the OID +func (c *ControlSyncInfo) GetControlType() string { + return ControlTypeSyncInfo +} + +// Encode encodes the control +func (c *ControlSyncInfo) Encode() *ber.Packet { + return nil +} + +// String returns a human-readable description +func (c *ControlSyncInfo) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t Value: %d %s %s %s %s", + ControlTypeMap[ControlTypeSyncInfo], + ControlTypeSyncInfo, + c.Criticality, + c.Value, + c.NewCookie, + c.RefreshDelete, + c.RefreshPresent, + c.SyncIdSet, + ) +} diff --git a/vendor/github.com/go-ldap/ldap/v3/debug.go b/vendor/github.com/go-ldap/ldap/v3/debug.go index d0a8fc15..6f89b4a0 100644 --- a/vendor/github.com/go-ldap/ldap/v3/debug.go +++ b/vendor/github.com/go-ldap/ldap/v3/debug.go @@ -1,13 +1,11 @@ package ldap import ( - "log" - ber "github.com/go-asn1-ber/asn1-ber" ) // debugging type -// - has a Printf method to write the debug output +// - has a Printf method to write the debug output type debugging bool // Enable controls debugging mode. @@ -18,13 +16,13 @@ func (debug *debugging) Enable(b bool) { // Printf writes debug output. func (debug debugging) Printf(format string, args ...interface{}) { if debug { - log.Printf(format, args...) + logger.Printf(format, args...) } } // PrintPacket dumps a packet. func (debug debugging) PrintPacket(packet *ber.Packet) { if debug { - ber.WritePacket(log.Writer(), packet) + ber.WritePacket(logger.Writer(), packet) } } diff --git a/vendor/github.com/go-ldap/ldap/v3/del.go b/vendor/github.com/go-ldap/ldap/v3/del.go index 6e987267..62306951 100644 --- a/vendor/github.com/go-ldap/ldap/v3/del.go +++ b/vendor/github.com/go-ldap/ldap/v3/del.go @@ -1,8 +1,7 @@ package ldap import ( - "log" - + "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) @@ -53,7 +52,8 @@ func (l *Conn) Del(delRequest *DelRequest) error { return err } } else { - log.Printf("Unexpected Response: %d", packet.Children[1].Tag) + return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) } + return nil } diff --git a/vendor/github.com/go-ldap/ldap/v3/dn.go b/vendor/github.com/go-ldap/ldap/v3/dn.go index 2b4cede9..6b485308 100644 --- a/vendor/github.com/go-ldap/ldap/v3/dn.go +++ b/vendor/github.com/go-ldap/ldap/v3/dn.go @@ -5,6 +5,7 @@ import ( enchex "encoding/hex" "errors" "fmt" + "sort" "strings" ber "github.com/go-asn1-ber/asn1-ber" @@ -18,16 +19,95 @@ type AttributeTypeAndValue struct { Value string } +// String returns a normalized string representation of this attribute type and +// value pair which is the a lowercased join of the Type and Value with a "=". +func (a *AttributeTypeAndValue) String() string { + return strings.ToLower(a.Type) + "=" + a.encodeValue() +} + +func (a *AttributeTypeAndValue) encodeValue() string { + // Normalize the value first. + // value := strings.ToLower(a.Value) + value := a.Value + + encodedBuf := bytes.Buffer{} + + escapeChar := func(c byte) { + encodedBuf.WriteByte('\\') + encodedBuf.WriteByte(c) + } + + escapeHex := func(c byte) { + encodedBuf.WriteByte('\\') + encodedBuf.WriteString(enchex.EncodeToString([]byte{c})) + } + + for i := 0; i < len(value); i++ { + char := value[i] + if i == 0 && char == ' ' || char == '#' { + // Special case leading space or number sign. + escapeChar(char) + continue + } + if i == len(value)-1 && char == ' ' { + // Special case trailing space. + escapeChar(char) + continue + } + + switch char { + case '"', '+', ',', ';', '<', '>', '\\': + // Each of these special characters must be escaped. + escapeChar(char) + continue + } + + if char < ' ' || char > '~' { + // All special character escapes are handled first + // above. All bytes less than ASCII SPACE and all bytes + // greater than ASCII TILDE must be hex-escaped. + escapeHex(char) + continue + } + + // Any other character does not require escaping. + encodedBuf.WriteByte(char) + } + + return encodedBuf.String() +} + // RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514 type RelativeDN struct { Attributes []*AttributeTypeAndValue } +// String returns a normalized string representation of this relative DN which +// is the a join of all attributes (sorted in increasing order) with a "+". +func (r *RelativeDN) String() string { + attrs := make([]string, len(r.Attributes)) + for i := range r.Attributes { + attrs[i] = r.Attributes[i].String() + } + sort.Strings(attrs) + return strings.Join(attrs, "+") +} + // DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514 type DN struct { RDNs []*RelativeDN } +// String returns a normalized string representation of this DN which is the +// join of all relative DNs with a ",". +func (d *DN) String() string { + rdns := make([]string, len(d.RDNs)) + for i := range d.RDNs { + rdns[i] = d.RDNs[i].String() + } + return strings.Join(rdns, ",") +} + // ParseDN returns a distinguishedName or an error. // The function respects https://tools.ietf.org/html/rfc4514 func ParseDN(str string) (*DN, error) { @@ -76,7 +156,7 @@ func ParseDN(str string) (*DN, error) { case char == '\\': unescapedTrailingSpaces = 0 escaping = true - case char == '=': + case char == '=' && attribute.Type == "": attribute.Type = stringFromBuffer() // Special case: If the first character in the value is # the // following data is BER encoded so we can just fast forward @@ -84,7 +164,7 @@ func ParseDN(str string) (*DN, error) { if len(str) > i+1 && str[i+1] == '#' { i += 2 index := strings.IndexAny(str[i:], ",+") - data := str + var data string if index > 0 { data = str[i : i+index] } else { @@ -101,7 +181,7 @@ func ParseDN(str string) (*DN, error) { buffer.WriteString(packet.Data.String()) i += len(data) - 1 } - case char == ',' || char == '+': + case char == ',' || char == '+' || char == ';': // We're done with this RDN or value, push it if len(attribute.Type) == 0 { return nil, errors.New("incomplete type, value pair") @@ -109,7 +189,7 @@ func ParseDN(str string) (*DN, error) { attribute.Value = stringFromBuffer() rdn.Attributes = append(rdn.Attributes, attribute) attribute = new(AttributeTypeAndValue) - if char == ',' { + if char == ',' || char == ';' { dn.RDNs = append(dn.RDNs, rdn) rdn = new(RelativeDN) rdn.Attributes = make([]*AttributeTypeAndValue, 0) @@ -206,7 +286,7 @@ func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool { return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value } -// Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). +// EqualFold returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). // Returns true if they have the same number of relative distinguished names // and corresponding relative distinguished names (by position) are the same. // Case of the attribute type and value is not significant @@ -238,7 +318,7 @@ func (d *DN) AncestorOfFold(other *DN) bool { return true } -// Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). +// EqualFold returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). // Case of the attribute type is not significant func (r *RelativeDN) EqualFold(other *RelativeDN) bool { if len(r.Attributes) != len(other.Attributes) { diff --git a/vendor/github.com/go-ldap/ldap/v3/error.go b/vendor/github.com/go-ldap/ldap/v3/error.go index 3cdb7b31..3c2559ef 100644 --- a/vendor/github.com/go-ldap/ldap/v3/error.go +++ b/vendor/github.com/go-ldap/ldap/v3/error.go @@ -192,6 +192,8 @@ func (e *Error) Error() string { return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error()) } +func (e *Error) Unwrap() error { return e.Err } + // GetLDAPError creates an Error out of a BER packet representing a LDAPResult // The return is an error object. It can be casted to a Error structure. // This function returns nil if resultCode in the LDAPResult sequence is success(0). @@ -206,15 +208,21 @@ func GetLDAPError(packet *ber.Packet) error { return &Error{ResultCode: ErrorUnexpectedResponse, Err: fmt.Errorf("Empty response in packet"), Packet: packet} } if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 { - resultCode := uint16(response.Children[0].Value.(int64)) - if resultCode == 0 { // No error - return nil - } - return &Error{ - ResultCode: resultCode, - MatchedDN: response.Children[1].Value.(string), - Err: fmt.Errorf("%s", response.Children[2].Value.(string)), - Packet: packet, + if ber.Type(response.Children[0].Tag) == ber.Type(ber.TagInteger) || ber.Type(response.Children[0].Tag) == ber.Type(ber.TagEnumerated) { + resultCode := uint16(response.Children[0].Value.(int64)) + if resultCode == 0 { // No error + return nil + } + + if ber.Type(response.Children[1].Tag) == ber.Type(ber.TagOctetString) && + ber.Type(response.Children[2].Tag) == ber.Type(ber.TagOctetString) { + return &Error{ + ResultCode: resultCode, + MatchedDN: response.Children[1].Value.(string), + Err: fmt.Errorf("%s", response.Children[2].Value.(string)), + Packet: packet, + } + } } } } diff --git a/vendor/github.com/go-ldap/ldap/v3/filter.go b/vendor/github.com/go-ldap/ldap/v3/filter.go index 73505e79..db76210c 100644 --- a/vendor/github.com/go-ldap/ldap/v3/filter.go +++ b/vendor/github.com/go-ldap/ldap/v3/filter.go @@ -396,7 +396,7 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) { case packet.Tag == FilterEqualityMatch && bytes.Equal(condition.Bytes(), _SymbolAny): packet = ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterPresent, attribute.String(), FilterMap[FilterPresent]) - case packet.Tag == FilterEqualityMatch && bytes.Index(condition.Bytes(), _SymbolAny) > -1: + case packet.Tag == FilterEqualityMatch && bytes.Contains(condition.Bytes(), _SymbolAny): packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute.String(), "Attribute")) packet.Tag = FilterSubstrings packet.Description = FilterMap[uint64(packet.Tag)] @@ -438,7 +438,6 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) { // Convert from "ABC\xx\xx\xx" form to literal bytes for transport func decodeEscapedSymbols(src []byte) (string, error) { - var ( buffer bytes.Buffer offset int diff --git a/vendor/github.com/go-ldap/ldap/v3/ldap.go b/vendor/github.com/go-ldap/ldap/v3/ldap.go index 7ae6dfe2..90837a77 100644 --- a/vendor/github.com/go-ldap/ldap/v3/ldap.go +++ b/vendor/github.com/go-ldap/ldap/v3/ldap.go @@ -3,7 +3,9 @@ package ldap import ( "fmt" "io/ioutil" + "log" "os" + "strings" ber "github.com/go-asn1-ber/asn1-ber" ) @@ -30,6 +32,7 @@ const ( ApplicationSearchResultReference = 19 ApplicationExtendedRequest = 23 ApplicationExtendedResponse = 24 + ApplicationIntermediateResponse = 25 ) // ApplicationMap contains human readable descriptions of LDAP Application Codes @@ -54,6 +57,7 @@ var ApplicationMap = map[uint8]string{ ApplicationSearchResultReference: "Search Result Reference", ApplicationExtendedRequest: "Extended Request", ApplicationExtendedResponse: "Extended Response", + ApplicationIntermediateResponse: "Intermediate Response", } // Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10) @@ -82,6 +86,13 @@ var BeheraPasswordPolicyErrorMap = map[int8]string{ BeheraPasswordInHistory: "New password is in list of old passwords", } +var logger = log.New(os.Stderr, "", log.LstdFlags) + +// Logger allows clients to override the default logger +func Logger(l *log.Logger) { + logger = l +} + // Adds descriptions to an LDAP Response packet for debugging func addLDAPDescriptions(packet *ber.Packet) (err error) { defer func() { @@ -221,18 +232,18 @@ func addControlDescriptions(packet *ber.Packet) error { sequence := value.Children[0] for _, child := range sequence.Children { if child.Tag == 0 { - //Warning + // Warning warningPacket := child.Children[0] val, err := ber.ParseInt64(warningPacket.Data.Bytes()) if err != nil { return fmt.Errorf("failed to decode data bytes: %s", err) } if warningPacket.Tag == 0 { - //timeBeforeExpiration + // timeBeforeExpiration value.Description += " (TimeBeforeExpiration)" warningPacket.Value = val } else if warningPacket.Tag == 1 { - //graceAuthNsRemaining + // graceAuthNsRemaining value.Description += " (GraceAuthNsRemaining)" warningPacket.Value = val } @@ -337,3 +348,43 @@ func EscapeFilter(filter string) string { } return string(buf) } + +// EscapeDN escapes distinguished names as described in RFC4514. Characters in the +// set `"+,;<>\` are escaped by prepending a backslash, which is also done for trailing +// spaces or a leading `#`. Null bytes are replaced with `\00`. +func EscapeDN(dn string) string { + if dn == "" { + return "" + } + + builder := strings.Builder{} + + for i, r := range dn { + // Escape leading and trailing spaces + if (i == 0 || i == len(dn)-1) && r == ' ' { + builder.WriteRune('\\') + builder.WriteRune(r) + continue + } + + // Escape leading '#' + if i == 0 && r == '#' { + builder.WriteRune('\\') + builder.WriteRune(r) + continue + } + + // Escape characters as defined in RFC4514 + switch r { + case '"', '+', ',', ';', '<', '>', '\\': + builder.WriteRune('\\') + builder.WriteRune(r) + case '\x00': // Null byte may not be escaped by a leading backslash + builder.WriteString("\\00") + default: + builder.WriteRune(r) + } + } + + return builder.String() +} diff --git a/vendor/github.com/go-ldap/ldap/v3/moddn.go b/vendor/github.com/go-ldap/ldap/v3/moddn.go index 71cdcd0b..84a6488e 100644 --- a/vendor/github.com/go-ldap/ldap/v3/moddn.go +++ b/vendor/github.com/go-ldap/ldap/v3/moddn.go @@ -1,8 +1,7 @@ package ldap import ( - "log" - + "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) @@ -25,7 +24,9 @@ type ModifyDNRequest struct { // RDN of the given DN. // // A call like -// mdnReq := NewModifyDNRequest("uid=someone,dc=example,dc=org", "uid=newname", true, "") +// +// mdnReq := NewModifyDNRequest("uid=someone,dc=example,dc=org", "uid=newname", true, "") +// // will setup the request to just rename uid=someone,dc=example,dc=org to // uid=newname,dc=example,dc=org. func NewModifyDNRequest(dn string, rdn string, delOld bool, newSup string) *ModifyDNRequest { @@ -94,7 +95,8 @@ func (l *Conn) ModifyDN(m *ModifyDNRequest) error { return err } } else { - log.Printf("Unexpected Response: %d", packet.Children[1].Tag) + return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) } + return nil } diff --git a/vendor/github.com/go-ldap/ldap/v3/modify.go b/vendor/github.com/go-ldap/ldap/v3/modify.go index 1821413d..0e501360 100644 --- a/vendor/github.com/go-ldap/ldap/v3/modify.go +++ b/vendor/github.com/go-ldap/ldap/v3/modify.go @@ -2,7 +2,7 @@ package ldap import ( "errors" - "log" + "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) @@ -127,8 +127,9 @@ func (l *Conn) Modify(modifyRequest *ModifyRequest) error { return err } } else { - log.Printf("Unexpected Response: %d", packet.Children[1].Tag) + return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) } + return nil } @@ -136,6 +137,8 @@ func (l *Conn) Modify(modifyRequest *ModifyRequest) error { type ModifyResult struct { // Controls are the returned controls Controls []Control + // Referral is the returned referral + Referral string } // ModifyWithResult performs the ModifyRequest and returns the result @@ -158,9 +161,10 @@ func (l *Conn) ModifyWithResult(modifyRequest *ModifyRequest) (*ModifyResult, er switch packet.Children[1].Tag { case ApplicationModifyResponse: - err := GetLDAPError(packet) - if err != nil { - return nil, err + if err = GetLDAPError(packet); err != nil { + result.Referral = getReferral(err, packet) + + return result, err } if len(packet.Children) == 3 { for _, child := range packet.Children[2].Children { diff --git a/vendor/github.com/go-ldap/ldap/v3/passwdmodify.go b/vendor/github.com/go-ldap/ldap/v3/passwdmodify.go index 62a11084..72a2351a 100644 --- a/vendor/github.com/go-ldap/ldap/v3/passwdmodify.go +++ b/vendor/github.com/go-ldap/ldap/v3/passwdmodify.go @@ -70,7 +70,6 @@ func (req *PasswordModifyRequest) appendTo(envelope *ber.Packet) error { // newPassword is the desired user's password. If empty the server can return // an error or generate a new password that will be available in the // PasswordModifyResult.GeneratedPassword -// func NewPasswordModifyRequest(userIdentity string, oldPassword string, newPassword string) *PasswordModifyRequest { return &PasswordModifyRequest{ UserIdentity: userIdentity, @@ -95,15 +94,9 @@ func (l *Conn) PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*Pa result := &PasswordModifyResult{} if packet.Children[1].Tag == ApplicationExtendedResponse { - err := GetLDAPError(packet) - if err != nil { - if IsErrorWithCode(err, LDAPResultReferral) { - for _, child := range packet.Children[1].Children { - if child.Tag == 3 { - result.Referral = child.Children[0].Value.(string) - } - } - } + if err = GetLDAPError(packet); err != nil { + result.Referral = getReferral(err, packet) + return result, err } } else { @@ -112,10 +105,10 @@ func (l *Conn) PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*Pa extendedResponse := packet.Children[1] for _, child := range extendedResponse.Children { - if child.Tag == 11 { + if child.Tag == ber.TagEmbeddedPDV { passwordModifyResponseValue := ber.DecodePacket(child.Data.Bytes()) if len(passwordModifyResponseValue.Children) == 1 { - if passwordModifyResponseValue.Children[0].Tag == 0 { + if passwordModifyResponseValue.Children[0].Tag == ber.TagEOC { result.GeneratedPassword = ber.DecodeString(passwordModifyResponseValue.Children[0].Data.Bytes()) } } diff --git a/vendor/github.com/go-ldap/ldap/v3/request.go b/vendor/github.com/go-ldap/ldap/v3/request.go index 4ea31e90..b64f232d 100644 --- a/vendor/github.com/go-ldap/ldap/v3/request.go +++ b/vendor/github.com/go-ldap/ldap/v3/request.go @@ -9,7 +9,8 @@ import ( var ( errRespChanClosed = errors.New("ldap: response channel closed") errCouldNotRetMsg = errors.New("ldap: could not retrieve message") - ErrNilConnection = errors.New("ldap: conn is nil, expected net.Conn") + // ErrNilConnection is returned if doRequest is called with a nil connection. + ErrNilConnection = errors.New("ldap: conn is nil, expected net.Conn") ) type request interface { @@ -69,3 +70,41 @@ func (l *Conn) readPacket(msgCtx *messageContext) (*ber.Packet, error) { } return packet, nil } + +func getReferral(err error, packet *ber.Packet) (referral string) { + if !IsErrorWithCode(err, LDAPResultReferral) { + return "" + } + + if len(packet.Children) < 2 { + return "" + } + + // The packet Tag itself (of child 2) is generally a ber.TagObjectDescriptor with referrals however OpenLDAP + // seemingly returns a ber.Tag.GeneralizedTime. Every currently tested LDAP server which returns referrals returns + // an ASN.1 BER packet with the Type of ber.TypeConstructed and Class of ber.ClassApplication however. Thus this + // check expressly checks these fields instead. + // + // Related Issues: + // - https://github.com/authelia/authelia/issues/4199 (downstream) + if len(packet.Children[1].Children) == 0 || (packet.Children[1].TagType != ber.TypeConstructed || packet.Children[1].ClassType != ber.ClassApplication) { + return "" + } + + var ok bool + + for _, child := range packet.Children[1].Children { + // The referral URI itself should be contained within a child which has a Tag of ber.BitString or + // ber.TagPrintableString, and the Type of ber.TypeConstructed and the Class of ClassContext. As soon as any of + // these conditions is not true we can skip this child. + if (child.Tag != ber.TagBitString && child.Tag != ber.TagPrintableString) || child.TagType != ber.TypeConstructed || child.ClassType != ber.ClassContext { + continue + } + + if referral, ok = child.Children[0].Value.(string); ok { + return referral + } + } + + return "" +} diff --git a/vendor/github.com/go-ldap/ldap/v3/response.go b/vendor/github.com/go-ldap/ldap/v3/response.go new file mode 100644 index 00000000..1abe02a3 --- /dev/null +++ b/vendor/github.com/go-ldap/ldap/v3/response.go @@ -0,0 +1,207 @@ +package ldap + +import ( + "context" + "errors" + "fmt" + + ber "github.com/go-asn1-ber/asn1-ber" +) + +// Response defines an interface to get data from an LDAP server +type Response interface { + Entry() *Entry + Referral() string + Controls() []Control + Err() error + Next() bool +} + +type searchResponse struct { + conn *Conn + ch chan *SearchSingleResult + + entry *Entry + referral string + controls []Control + err error +} + +// Entry returns an entry from the given search request +func (r *searchResponse) Entry() *Entry { + return r.entry +} + +// Referral returns a referral from the given search request +func (r *searchResponse) Referral() string { + return r.referral +} + +// Controls returns controls from the given search request +func (r *searchResponse) Controls() []Control { + return r.controls +} + +// Err returns an error when the given search request was failed +func (r *searchResponse) Err() error { + return r.err +} + +// Next returns whether next data exist or not +func (r *searchResponse) Next() bool { + res, ok := <-r.ch + if !ok { + return false + } + if res == nil { + return false + } + r.err = res.Error + if r.err != nil { + return false + } + r.entry = res.Entry + r.referral = res.Referral + r.controls = res.Controls + return true +} + +func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest) { + go func() { + defer func() { + close(r.ch) + if err := recover(); err != nil { + r.conn.err = fmt.Errorf("ldap: recovered panic in searchResponse: %v", err) + } + }() + + if r.conn.IsClosing() { + return + } + + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, r.conn.nextMessageID(), "MessageID")) + // encode search request + err := searchRequest.appendTo(packet) + if err != nil { + r.ch <- &SearchSingleResult{Error: err} + return + } + r.conn.Debug.PrintPacket(packet) + + msgCtx, err := r.conn.sendMessage(packet) + if err != nil { + r.ch <- &SearchSingleResult{Error: err} + return + } + defer r.conn.finishMessage(msgCtx) + + foundSearchSingleResultDone := false + for !foundSearchSingleResultDone { + select { + case <-ctx.Done(): + r.conn.Debug.Printf("%d: %s", msgCtx.id, ctx.Err().Error()) + return + default: + r.conn.Debug.Printf("%d: waiting for response", msgCtx.id) + packetResponse, ok := <-msgCtx.responses + if !ok { + err := NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + r.ch <- &SearchSingleResult{Error: err} + return + } + packet, err = packetResponse.ReadPacket() + r.conn.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + r.ch <- &SearchSingleResult{Error: err} + return + } + + if r.conn.Debug { + if err := addLDAPDescriptions(packet); err != nil { + r.ch <- &SearchSingleResult{Error: err} + return + } + ber.PrintPacket(packet) + } + + switch packet.Children[1].Tag { + case ApplicationSearchResultEntry: + result := &SearchSingleResult{ + Entry: &Entry{ + DN: packet.Children[1].Children[0].Value.(string), + Attributes: unpackAttributes(packet.Children[1].Children[1].Children), + }, + } + if len(packet.Children) != 3 { + r.ch <- result + continue + } + decoded, err := DecodeControl(packet.Children[2].Children[0]) + if err != nil { + werr := fmt.Errorf("failed to decode search result entry: %w", err) + result.Error = werr + r.ch <- result + return + } + result.Controls = append(result.Controls, decoded) + r.ch <- result + + case ApplicationSearchResultDone: + if err := GetLDAPError(packet); err != nil { + r.ch <- &SearchSingleResult{Error: err} + return + } + if len(packet.Children) == 3 { + result := &SearchSingleResult{} + for _, child := range packet.Children[2].Children { + decodedChild, err := DecodeControl(child) + if err != nil { + werr := fmt.Errorf("failed to decode child control: %w", err) + r.ch <- &SearchSingleResult{Error: werr} + return + } + result.Controls = append(result.Controls, decodedChild) + } + r.ch <- result + } + foundSearchSingleResultDone = true + + case ApplicationSearchResultReference: + ref := packet.Children[1].Children[0].Value.(string) + r.ch <- &SearchSingleResult{Referral: ref} + + case ApplicationIntermediateResponse: + decoded, err := DecodeControl(packet.Children[1]) + if err != nil { + werr := fmt.Errorf("failed to decode intermediate response: %w", err) + r.ch <- &SearchSingleResult{Error: werr} + return + } + result := &SearchSingleResult{} + result.Controls = append(result.Controls, decoded) + r.ch <- result + + default: + err := fmt.Errorf("unknown tag: %d", packet.Children[1].Tag) + r.ch <- &SearchSingleResult{Error: err} + return + } + } + } + r.conn.Debug.Printf("%d: returning", msgCtx.id) + }() +} + +func newSearchResponse(conn *Conn, bufferSize int) *searchResponse { + var ch chan *SearchSingleResult + if bufferSize > 0 { + ch = make(chan *SearchSingleResult, bufferSize) + } else { + ch = make(chan *SearchSingleResult) + } + return &searchResponse{ + conn: conn, + ch: ch, + } +} diff --git a/vendor/github.com/go-ldap/ldap/v3/search.go b/vendor/github.com/go-ldap/ldap/v3/search.go index 915e4203..4eb10762 100644 --- a/vendor/github.com/go-ldap/ldap/v3/search.go +++ b/vendor/github.com/go-ldap/ldap/v3/search.go @@ -1,10 +1,14 @@ package ldap import ( + "context" "errors" "fmt" + "reflect" "sort" + "strconv" "strings" + "time" ber "github.com/go-asn1-ber/asn1-ber" ) @@ -161,6 +165,155 @@ func (e *Entry) PrettyPrint(indent int) { } } +// Describe the tag to use for struct field tags +const decoderTagName = "ldap" + +// readTag will read the reflect.StructField value for +// the key defined in decoderTagName. If omitempty is +// specified, the field may not be filled. +func readTag(f reflect.StructField) (string, bool) { + val, ok := f.Tag.Lookup(decoderTagName) + if !ok { + return f.Name, false + } + opts := strings.Split(val, ",") + omit := false + if len(opts) == 2 { + omit = opts[1] == "omitempty" + } + return opts[0], omit +} + +// Unmarshal parses the Entry in the value pointed to by i +// +// Currently, this methods only supports struct fields of type +// string, []string, int, int64, []byte, *DN, []*DN or time.Time. Other field types +// will not be regarded. If the field type is a string or int but multiple +// attribute values are returned, the first value will be used to fill the field. +// +// Example: +// +// type UserEntry struct { +// // Fields with the tag key `dn` are automatically filled with the +// // objects distinguishedName. This can be used multiple times. +// DN string `ldap:"dn"` +// +// // This field will be filled with the attribute value for +// // userPrincipalName. An attribute can be read into a struct field +// // multiple times. Missing attributes will not result in an error. +// UserPrincipalName string `ldap:"userPrincipalName"` +// +// // memberOf may have multiple values. If you don't +// // know the amount of attribute values at runtime, use a string array. +// MemberOf []string `ldap:"memberOf"` +// +// // ID is an integer value, it will fail unmarshaling when the given +// // attribute value cannot be parsed into an integer. +// ID int `ldap:"id"` +// +// // LongID is similar to ID but uses an int64 instead. +// LongID int64 `ldap:"longId"` +// +// // Data is similar to MemberOf a slice containing all attribute +// // values. +// Data []byte `ldap:"data"` +// +// // Time is parsed with the generalizedTime spec into a time.Time +// Created time.Time `ldap:"createdTimestamp"` +// +// // *DN is parsed with the ParseDN +// Owner *ldap.DN `ldap:"owner"` +// +// // []*DN is parsed with the ParseDN +// Children []*ldap.DN `ldap:"children"` +// +// // This won't work, as the field is not of type string. For this +// // to work, you'll have to temporarily store the result in string +// // (or string array) and convert it to the desired type afterwards. +// UserAccountControl uint32 `ldap:"userPrincipalName"` +// } +// user := UserEntry{} +// +// if err := result.Unmarshal(&user); err != nil { +// // ... +// } +func (e *Entry) Unmarshal(i interface{}) (err error) { + // Make sure it's a ptr + if vo := reflect.ValueOf(i).Kind(); vo != reflect.Ptr { + return fmt.Errorf("ldap: cannot use %s, expected pointer to a struct", vo) + } + + sv, st := reflect.ValueOf(i).Elem(), reflect.TypeOf(i).Elem() + // Make sure it's pointing to a struct + if sv.Kind() != reflect.Struct { + return fmt.Errorf("ldap: expected pointer to a struct, got %s", sv.Kind()) + } + + for n := 0; n < st.NumField(); n++ { + // Holds struct field value and type + fv, ft := sv.Field(n), st.Field(n) + + // skip unexported fields + if ft.PkgPath != "" { + continue + } + + // omitempty can be safely discarded, as it's not needed when unmarshalling + fieldTag, _ := readTag(ft) + + // Fill the field with the distinguishedName if the tag key is `dn` + if fieldTag == "dn" { + fv.SetString(e.DN) + continue + } + + values := e.GetAttributeValues(fieldTag) + if len(values) == 0 { + continue + } + + switch fv.Interface().(type) { + case []string: + for _, item := range values { + fv.Set(reflect.Append(fv, reflect.ValueOf(item))) + } + case string: + fv.SetString(values[0]) + case []byte: + fv.SetBytes([]byte(values[0])) + case int, int64: + intVal, err := strconv.ParseInt(values[0], 10, 64) + if err != nil { + return fmt.Errorf("ldap: could not parse value '%s' into int field", values[0]) + } + fv.SetInt(intVal) + case time.Time: + t, err := ber.ParseGeneralizedTime([]byte(values[0])) + if err != nil { + return fmt.Errorf("ldap: could not parse value '%s' into time.Time field", values[0]) + } + fv.Set(reflect.ValueOf(t)) + case *DN: + dn, err := ParseDN(values[0]) + if err != nil { + return fmt.Errorf("ldap: could not parse value '%s' into *ldap.DN field", values[0]) + } + fv.Set(reflect.ValueOf(dn)) + case []*DN: + for _, item := range values { + dn, err := ParseDN(item) + if err != nil { + return fmt.Errorf("ldap: could not parse value '%s' into *ldap.DN field", item) + } + fv.Set(reflect.Append(fv, reflect.ValueOf(dn))) + } + default: + return fmt.Errorf("ldap: expected field to be of type string, []string, int, int64, []byte, *DN, []*DN or time.Time, got %v", ft.Type) + } + } + return +} + // NewEntryAttribute returns a new EntryAttribute with the desired key-value pair func NewEntryAttribute(name string, values []string) *EntryAttribute { var bytes [][]byte @@ -218,6 +371,35 @@ func (s *SearchResult) PrettyPrint(indent int) { } } +// appendTo appends all entries of `s` to `r` +func (s *SearchResult) appendTo(r *SearchResult) { + r.Entries = append(r.Entries, s.Entries...) + r.Referrals = append(r.Referrals, s.Referrals...) + r.Controls = append(r.Controls, s.Controls...) +} + +// SearchSingleResult holds the server's single entry response to a search request +type SearchSingleResult struct { + // Entry is the returned entry + Entry *Entry + // Referral is the returned referral + Referral string + // Controls are the returned controls + Controls []Control + // Error is set when the search request was failed + Error error +} + +// Print outputs a human-readable description +func (s *SearchSingleResult) Print() { + s.Entry.Print() +} + +// PrettyPrint outputs a human-readable description with indenting +func (s *SearchSingleResult) PrettyPrint(indent int) { + s.Entry.PrettyPrint(indent) +} + // SearchRequest represents a search request to send to the server type SearchRequest struct { BaseDN string @@ -285,10 +467,11 @@ func NewSearchRequest( // SearchWithPaging accepts a search request and desired page size in order to execute LDAP queries to fulfill the // search request. All paged LDAP query responses will be buffered and the final result will be returned atomically. // The following four cases are possible given the arguments: -// - given SearchRequest missing a control of type ControlTypePaging: we will add one with the desired paging size -// - given SearchRequest contains a control of type ControlTypePaging that isn't actually a ControlPaging: fail without issuing any queries -// - given SearchRequest contains a control of type ControlTypePaging with pagingSize equal to the size requested: no change to the search request -// - given SearchRequest contains a control of type ControlTypePaging with pagingSize not equal to the size requested: fail without issuing any queries +// - given SearchRequest missing a control of type ControlTypePaging: we will add one with the desired paging size +// - given SearchRequest contains a control of type ControlTypePaging that isn't actually a ControlPaging: fail without issuing any queries +// - given SearchRequest contains a control of type ControlTypePaging with pagingSize equal to the size requested: no change to the search request +// - given SearchRequest contains a control of type ControlTypePaging with pagingSize not equal to the size requested: fail without issuing any queries +// // A requested pagingSize of 0 is interpreted as no limit by LDAP servers. func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) { var pagingControl *ControlPaging @@ -311,23 +494,19 @@ func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) searchResult := new(SearchResult) for { result, err := l.Search(searchRequest) - l.Debug.Printf("Looking for Paging Control...") + if result != nil { + result.appendTo(searchResult) + } else { + if err == nil { + // We have to do this beautifulness in case something absolutely strange happens, which + // should only occur in case there is no packet, but also no error. + return searchResult, NewError(ErrorNetwork, errors.New("ldap: packet not received")) + } + } if err != nil { + // If an error occurred, all results that have been received so far will be returned return searchResult, err } - if result == nil { - return searchResult, NewError(ErrorNetwork, errors.New("ldap: packet not received")) - } - - for _, entry := range result.Entries { - searchResult.Entries = append(searchResult.Entries, entry) - } - for _, referral := range result.Referrals { - searchResult.Referrals = append(searchResult.Referrals, referral) - } - for _, control := range result.Controls { - searchResult.Controls = append(searchResult.Controls, control) - } l.Debug.Printf("Looking for Paging Control...") pagingResult := FindControl(result.Controls, ControlTypePaging) @@ -349,7 +528,9 @@ func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) if pagingControl != nil { l.Debug.Printf("Abandoning Paging...") pagingControl.PagingSize = 0 - l.Search(searchRequest) + if _, err := l.Search(searchRequest); err != nil { + return searchResult, err + } } return searchResult, nil @@ -366,7 +547,8 @@ func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { result := &SearchResult{ Entries: make([]*Entry, 0), Referrals: make([]string, 0), - Controls: make([]Control, 0)} + Controls: make([]Control, 0), + } for { packet, err := l.readPacket(msgCtx) @@ -402,6 +584,32 @@ func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { } } +// SearchAsync performs a search request and returns all search results asynchronously. +// This means you get all results until an error happens (or the search successfully finished), +// e.g. for size / time limited requests all are recieved until the limit is reached. +// To stop the search, call cancel function of the context. +func (l *Conn) SearchAsync( + ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response { + r := newSearchResponse(l, bufferSize) + r.start(ctx, searchRequest) + return r +} + +// Syncrepl is a short name for LDAP Sync Replication engine that works on the +// consumer-side. This can perform a persistent search and returns an entry +// when the entry is updated on the server side. +// To stop the search, call cancel function of the context. +func (l *Conn) Syncrepl( + ctx context.Context, searchRequest *SearchRequest, bufferSize int, + mode ControlSyncRequestMode, cookie []byte, reloadHint bool, +) Response { + control := NewControlSyncRequest(mode, cookie, reloadHint) + searchRequest.Controls = append(searchRequest.Controls, control) + r := newSearchResponse(l, bufferSize) + r.start(ctx, searchRequest) + return r +} + // unpackAttributes will extract all given LDAP attributes and it's values // from the ber.Packet func unpackAttributes(children []*ber.Packet) []*EntryAttribute { @@ -425,3 +633,58 @@ func unpackAttributes(children []*ber.Packet) []*EntryAttribute { return entries } + +// DirSync does a Search with dirSync Control. +func (l *Conn) DirSync( + searchRequest *SearchRequest, flags int64, maxAttrCount int64, cookie []byte, +) (*SearchResult, error) { + control := FindControl(searchRequest.Controls, ControlTypeDirSync) + if control == nil { + c := NewRequestControlDirSync(flags, maxAttrCount, cookie) + searchRequest.Controls = append(searchRequest.Controls, c) + } else { + c := control.(*ControlDirSync) + if c.Flags != flags { + return nil, fmt.Errorf("flags given in search request (%d) conflicts with flags given in search call (%d)", c.Flags, flags) + } + if c.MaxAttrCount != maxAttrCount { + return nil, fmt.Errorf("MaxAttrCnt given in search request (%d) conflicts with maxAttrCount given in search call (%d)", c.MaxAttrCount, maxAttrCount) + } + } + searchResult, err := l.Search(searchRequest) + l.Debug.Printf("Looking for result...") + if err != nil { + return nil, err + } + if searchResult == nil { + return nil, NewError(ErrorNetwork, errors.New("ldap: packet not received")) + } + + l.Debug.Printf("Looking for DirSync Control...") + resultControl := FindControl(searchResult.Controls, ControlTypeDirSync) + if resultControl == nil { + l.Debug.Printf("Could not find dirSyncControl control. Breaking...") + return searchResult, nil + } + + cookie = resultControl.(*ControlDirSync).Cookie + if len(cookie) == 0 { + l.Debug.Printf("Could not find cookie. Breaking...") + return searchResult, nil + } + + return searchResult, nil +} + +// DirSyncDirSyncAsync performs a search request and returns all search results +// asynchronously. This is efficient when the server returns lots of entries. +func (l *Conn) DirSyncAsync( + ctx context.Context, searchRequest *SearchRequest, bufferSize int, + flags, maxAttrCount int64, cookie []byte, +) Response { + control := NewRequestControlDirSync(flags, maxAttrCount, cookie) + searchRequest.Controls = append(searchRequest.Controls, control) + r := newSearchResponse(l, bufferSize) + r.start(ctx, searchRequest) + return r +} diff --git a/vendor/github.com/go-ldap/ldap/v3/unbind.go b/vendor/github.com/go-ldap/ldap/v3/unbind.go index 6c411cd1..10cf75c6 100644 --- a/vendor/github.com/go-ldap/ldap/v3/unbind.go +++ b/vendor/github.com/go-ldap/ldap/v3/unbind.go @@ -6,6 +6,7 @@ import ( ber "github.com/go-asn1-ber/asn1-ber" ) +// ErrConnUnbound is returned when Unbind is called on an already closing connection. var ErrConnUnbound = NewError(ErrorNetwork, errors.New("ldap: connection is closed")) type unbindRequest struct{} diff --git a/vendor/github.com/go-sql-driver/mysql/AUTHORS b/vendor/github.com/go-sql-driver/mysql/AUTHORS index 50afa2c8..fb1478c3 100644 --- a/vendor/github.com/go-sql-driver/mysql/AUTHORS +++ b/vendor/github.com/go-sql-driver/mysql/AUTHORS @@ -23,6 +23,7 @@ Asta Xie Bulat Gaifullin Caine Jette Carlos Nieto +Chris Kirkland Chris Moos Craig Wilson Daniel Montoya @@ -45,6 +46,7 @@ Ilia Cimpoes INADA Naoki Jacek Szwec James Harr +Janek Vedock Jeff Hodges Jeffrey Charles Jerome Meyer @@ -59,12 +61,14 @@ Kamil Dziedzic Kei Kamikawa Kevin Malachowski Kieron Woodhouse +Lance Tian Lennart Rudolph Leonardo YongUk Kim Linh Tran Tuan Lion Yang Luca Looz Lucas Liu +Lunny Xiao Luke Scott Maciej Zimnoch Michael Woolnough @@ -74,11 +78,14 @@ Olivier Mengué oscarzhao Paul Bonser Peter Schultz +Phil Porada Rebecca Chin Reed Allman Richard Wilkes Robert Russell Runrioter Wung +Samantha Frank +Santhosh Kumar Tekuri Sho Iizuka Sho Ikeda Shuode Li @@ -99,12 +106,14 @@ Xiuming Chen Xuehong Chan Zhenye Xie Zhixin Wen +Ziheng Lyu # Organizations Barracuda Networks, Inc. Counting Ltd. DigitalOcean Inc. +dyves labs AG Facebook Inc. GitHub Inc. Google Inc. diff --git a/vendor/github.com/go-sql-driver/mysql/CHANGELOG.md b/vendor/github.com/go-sql-driver/mysql/CHANGELOG.md index 72a738ed..5166e4ad 100644 --- a/vendor/github.com/go-sql-driver/mysql/CHANGELOG.md +++ b/vendor/github.com/go-sql-driver/mysql/CHANGELOG.md @@ -1,3 +1,37 @@ +## Version 1.7.1 (2023-04-25) + +Changes: + + - bump actions/checkout@v3 and actions/setup-go@v3 (#1375) + - Add go1.20 and mariadb10.11 to the testing matrix (#1403) + - Increase default maxAllowedPacket size. (#1411) + +Bugfixes: + + - Use SET syntax as specified in the MySQL documentation (#1402) + + +## Version 1.7 (2022-11-29) + +Changes: + + - Drop support of Go 1.12 (#1211) + - Refactoring `(*textRows).readRow` in a more clear way (#1230) + - util: Reduce boundary check in escape functions. (#1316) + - enhancement for mysqlConn handleAuthResult (#1250) + +New Features: + + - support Is comparison on MySQLError (#1210) + - return unsigned in database type name when necessary (#1238) + - Add API to express like a --ssl-mode=PREFERRED MySQL client (#1370) + - Add SQLState to MySQLError (#1321) + +Bugfixes: + + - Fix parsing 0 year. (#1257) + + ## Version 1.6 (2021-04-01) Changes: diff --git a/vendor/github.com/go-sql-driver/mysql/README.md b/vendor/github.com/go-sql-driver/mysql/README.md index 0b13154f..3b5d229a 100644 --- a/vendor/github.com/go-sql-driver/mysql/README.md +++ b/vendor/github.com/go-sql-driver/mysql/README.md @@ -40,7 +40,7 @@ A MySQL-Driver for Go's [database/sql](https://golang.org/pkg/database/sql/) pac * Optional placeholder interpolation ## Requirements - * Go 1.10 or higher. We aim to support the 3 latest versions of Go. + * Go 1.13 or higher. We aim to support the 3 latest versions of Go. * MySQL (4.1+), MariaDB, Percona Server, Google CloudSQL or Sphinx (2.2.3+) --------------------------------------- @@ -85,7 +85,7 @@ db.SetMaxIdleConns(10) `db.SetMaxOpenConns()` is highly recommended to limit the number of connection used by the application. There is no recommended limit number because it depends on application and MySQL server. -`db.SetMaxIdleConns()` is recommended to be set same to (or greater than) `db.SetMaxOpenConns()`. When it is smaller than `SetMaxOpenConns()`, connections can be opened and closed very frequently than you expect. Idle connections can be closed by the `db.SetConnMaxLifetime()`. If you want to close idle connections more rapidly, you can use `db.SetConnMaxIdleTime()` since Go 1.15. +`db.SetMaxIdleConns()` is recommended to be set same to `db.SetMaxOpenConns()`. When it is smaller than `SetMaxOpenConns()`, connections can be opened and closed much more frequently than you expect. Idle connections can be closed by the `db.SetConnMaxLifetime()`. If you want to close idle connections more rapidly, you can use `db.SetConnMaxIdleTime()` since Go 1.15. ### DSN (Data Source Name) @@ -157,6 +157,17 @@ Default: false `allowCleartextPasswords=true` allows using the [cleartext client side plugin](https://dev.mysql.com/doc/en/cleartext-pluggable-authentication.html) if required by an account, such as one defined with the [PAM authentication plugin](http://dev.mysql.com/doc/en/pam-authentication-plugin.html). Sending passwords in clear text may be a security problem in some configurations. To avoid problems if there is any possibility that the password would be intercepted, clients should connect to MySQL Server using a method that protects the password. Possibilities include [TLS / SSL](#tls), IPsec, or a private network. + +##### `allowFallbackToPlaintext` + +``` +Type: bool +Valid Values: true, false +Default: false +``` + +`allowFallbackToPlaintext=true` acts like a `--ssl-mode=PREFERRED` MySQL client as described in [Command Options for Connecting to the Server](https://dev.mysql.com/doc/refman/5.7/en/connection-options.html#option_general_ssl-mode) + ##### `allowNativePasswords` ``` @@ -271,10 +282,10 @@ Please keep in mind, that param values must be [url.QueryEscape](https://golang. ##### `maxAllowedPacket` ``` Type: decimal number -Default: 4194304 +Default: 64*1024*1024 ``` -Max packet size allowed in bytes. The default value is 4 MiB and should be adjusted to match the server settings. `maxAllowedPacket=0` can be used to automatically fetch the `max_allowed_packet` variable from server *on every connection*. +Max packet size allowed in bytes. The default value is 64 MiB and should be adjusted to match the server settings. `maxAllowedPacket=0` can be used to automatically fetch the `max_allowed_packet` variable from server *on every connection*. ##### `multiStatements` @@ -454,7 +465,7 @@ user:password@/ The connection pool is managed by Go's database/sql package. For details on how to configure the size of the pool and how long connections stay in the pool see `*DB.SetMaxOpenConns`, `*DB.SetMaxIdleConns`, and `*DB.SetConnMaxLifetime` in the [database/sql documentation](https://golang.org/pkg/database/sql/). The read, write, and dial timeouts for each individual connection are configured with the DSN parameters [`readTimeout`](#readtimeout), [`writeTimeout`](#writetimeout), and [`timeout`](#timeout), respectively. ## `ColumnType` Support -This driver supports the [`ColumnType` interface](https://golang.org/pkg/database/sql/#ColumnType) introduced in Go 1.8, with the exception of [`ColumnType.Length()`](https://golang.org/pkg/database/sql/#ColumnType.Length), which is currently not supported. +This driver supports the [`ColumnType` interface](https://golang.org/pkg/database/sql/#ColumnType) introduced in Go 1.8, with the exception of [`ColumnType.Length()`](https://golang.org/pkg/database/sql/#ColumnType.Length), which is currently not supported. All Unsigned database type names will be returned `UNSIGNED ` with `INT`, `TINYINT`, `SMALLINT`, `BIGINT`. ## `context.Context` Support Go 1.8 added `database/sql` support for `context.Context`. This driver supports query timeouts and cancellation via contexts. diff --git a/vendor/github.com/go-sql-driver/mysql/atomic_bool.go b/vendor/github.com/go-sql-driver/mysql/atomic_bool.go new file mode 100644 index 00000000..1b7e19f3 --- /dev/null +++ b/vendor/github.com/go-sql-driver/mysql/atomic_bool.go @@ -0,0 +1,19 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package. +// +// Copyright 2022 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. +//go:build go1.19 +// +build go1.19 + +package mysql + +import "sync/atomic" + +/****************************************************************************** +* Sync utils * +******************************************************************************/ + +type atomicBool = atomic.Bool diff --git a/vendor/github.com/go-sql-driver/mysql/atomic_bool_go118.go b/vendor/github.com/go-sql-driver/mysql/atomic_bool_go118.go new file mode 100644 index 00000000..2e9a7f0b --- /dev/null +++ b/vendor/github.com/go-sql-driver/mysql/atomic_bool_go118.go @@ -0,0 +1,47 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package. +// +// Copyright 2022 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. +//go:build !go1.19 +// +build !go1.19 + +package mysql + +import "sync/atomic" + +/****************************************************************************** +* Sync utils * +******************************************************************************/ + +// atomicBool is an implementation of atomic.Bool for older version of Go. +// it is a wrapper around uint32 for usage as a boolean value with +// atomic access. +type atomicBool struct { + _ noCopy + value uint32 +} + +// Load returns whether the current boolean value is true +func (ab *atomicBool) Load() bool { + return atomic.LoadUint32(&ab.value) > 0 +} + +// Store sets the value of the bool regardless of the previous value +func (ab *atomicBool) Store(value bool) { + if value { + atomic.StoreUint32(&ab.value, 1) + } else { + atomic.StoreUint32(&ab.value, 0) + } +} + +// Swap sets the value of the bool and returns the old value. +func (ab *atomicBool) Swap(value bool) bool { + if value { + return atomic.SwapUint32(&ab.value, 1) > 0 + } + return atomic.SwapUint32(&ab.value, 0) > 0 +} diff --git a/vendor/github.com/go-sql-driver/mysql/auth.go b/vendor/github.com/go-sql-driver/mysql/auth.go index b2f19e8f..1ff203e5 100644 --- a/vendor/github.com/go-sql-driver/mysql/auth.go +++ b/vendor/github.com/go-sql-driver/mysql/auth.go @@ -33,27 +33,26 @@ var ( // Note: The provided rsa.PublicKey instance is exclusively owned by the driver // after registering it and may not be modified. // -// data, err := ioutil.ReadFile("mykey.pem") -// if err != nil { -// log.Fatal(err) -// } +// data, err := ioutil.ReadFile("mykey.pem") +// if err != nil { +// log.Fatal(err) +// } // -// block, _ := pem.Decode(data) -// if block == nil || block.Type != "PUBLIC KEY" { -// log.Fatal("failed to decode PEM block containing public key") -// } +// block, _ := pem.Decode(data) +// if block == nil || block.Type != "PUBLIC KEY" { +// log.Fatal("failed to decode PEM block containing public key") +// } // -// pub, err := x509.ParsePKIXPublicKey(block.Bytes) -// if err != nil { -// log.Fatal(err) -// } -// -// if rsaPubKey, ok := pub.(*rsa.PublicKey); ok { -// mysql.RegisterServerPubKey("mykey", rsaPubKey) -// } else { -// log.Fatal("not a RSA public key") -// } +// pub, err := x509.ParsePKIXPublicKey(block.Bytes) +// if err != nil { +// log.Fatal(err) +// } // +// if rsaPubKey, ok := pub.(*rsa.PublicKey); ok { +// mysql.RegisterServerPubKey("mykey", rsaPubKey) +// } else { +// log.Fatal("not a RSA public key") +// } func RegisterServerPubKey(name string, pubKey *rsa.PublicKey) { serverPubKeyLock.Lock() if serverPubKeyRegistry == nil { @@ -274,7 +273,9 @@ func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) { if len(mc.cfg.Passwd) == 0 { return []byte{0}, nil } - if mc.cfg.tls != nil || mc.cfg.Net == "unix" { + // unlike caching_sha2_password, sha256_password does not accept + // cleartext password on unix transport. + if mc.cfg.TLS != nil { // write cleartext auth packet return append([]byte(mc.cfg.Passwd), 0), nil } @@ -350,7 +351,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error { } case cachingSha2PasswordPerformFullAuthentication: - if mc.cfg.tls != nil || mc.cfg.Net == "unix" { + if mc.cfg.TLS != nil || mc.cfg.Net == "unix" { // write cleartext auth packet err = mc.writeAuthSwitchPacket(append([]byte(mc.cfg.Passwd), 0)) if err != nil { @@ -365,13 +366,20 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error { return err } data[4] = cachingSha2PasswordRequestPublicKey - mc.writePacket(data) + err = mc.writePacket(data) + if err != nil { + return err + } - // parse public key if data, err = mc.readPacket(); err != nil { return err } + if data[0] != iAuthMoreData { + return fmt.Errorf("unexpect resp from server for caching_sha2_password perform full authentication") + } + + // parse public key block, rest := pem.Decode(data[1:]) if block == nil { return fmt.Errorf("No Pem data found, data: %s", rest) @@ -404,6 +412,10 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error { return nil // auth successful default: block, _ := pem.Decode(authData) + if block == nil { + return fmt.Errorf("no Pem data found, data: %s", authData) + } + pub, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return err diff --git a/vendor/github.com/go-sql-driver/mysql/collations.go b/vendor/github.com/go-sql-driver/mysql/collations.go index 326a9f7f..295bfbe5 100644 --- a/vendor/github.com/go-sql-driver/mysql/collations.go +++ b/vendor/github.com/go-sql-driver/mysql/collations.go @@ -13,7 +13,8 @@ const binaryCollation = "binary" // A list of available collations mapped to the internal ID. // To update this map use the following MySQL query: -// SELECT COLLATION_NAME, ID FROM information_schema.COLLATIONS WHERE ID<256 ORDER BY ID +// +// SELECT COLLATION_NAME, ID FROM information_schema.COLLATIONS WHERE ID<256 ORDER BY ID // // Handshake packet have only 1 byte for collation_id. So we can't use collations with ID > 255. // diff --git a/vendor/github.com/go-sql-driver/mysql/conncheck.go b/vendor/github.com/go-sql-driver/mysql/conncheck.go index 024eb285..0ea72172 100644 --- a/vendor/github.com/go-sql-driver/mysql/conncheck.go +++ b/vendor/github.com/go-sql-driver/mysql/conncheck.go @@ -6,6 +6,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at http://mozilla.org/MPL/2.0/. +//go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos // +build linux darwin dragonfly freebsd netbsd openbsd solaris illumos package mysql diff --git a/vendor/github.com/go-sql-driver/mysql/conncheck_dummy.go b/vendor/github.com/go-sql-driver/mysql/conncheck_dummy.go index ea7fb607..a56c138f 100644 --- a/vendor/github.com/go-sql-driver/mysql/conncheck_dummy.go +++ b/vendor/github.com/go-sql-driver/mysql/conncheck_dummy.go @@ -6,6 +6,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at http://mozilla.org/MPL/2.0/. +//go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !illumos // +build !linux,!darwin,!dragonfly,!freebsd,!netbsd,!openbsd,!solaris,!illumos package mysql diff --git a/vendor/github.com/go-sql-driver/mysql/connection.go b/vendor/github.com/go-sql-driver/mysql/connection.go index 835f8972..947a883e 100644 --- a/vendor/github.com/go-sql-driver/mysql/connection.go +++ b/vendor/github.com/go-sql-driver/mysql/connection.go @@ -71,10 +71,10 @@ func (mc *mysqlConn) handleParams() (err error) { cmdSet.Grow(4 + len(param) + 1 + len(val) + 30*(len(mc.cfg.Params)-1)) cmdSet.WriteString("SET ") } else { - cmdSet.WriteByte(',') + cmdSet.WriteString(", ") } cmdSet.WriteString(param) - cmdSet.WriteByte('=') + cmdSet.WriteString(" = ") cmdSet.WriteString(val) } } @@ -104,7 +104,7 @@ func (mc *mysqlConn) Begin() (driver.Tx, error) { } func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) { - if mc.closed.IsSet() { + if mc.closed.Load() { errLog.Print(ErrInvalidConn) return nil, driver.ErrBadConn } @@ -123,7 +123,7 @@ func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) { func (mc *mysqlConn) Close() (err error) { // Makes Close idempotent - if !mc.closed.IsSet() { + if !mc.closed.Load() { err = mc.writeCommandPacket(comQuit) } @@ -137,7 +137,7 @@ func (mc *mysqlConn) Close() (err error) { // is called before auth or on auth failure because MySQL will have already // closed the network connection. func (mc *mysqlConn) cleanup() { - if !mc.closed.TrySet(true) { + if mc.closed.Swap(true) { return } @@ -152,7 +152,7 @@ func (mc *mysqlConn) cleanup() { } func (mc *mysqlConn) error() error { - if mc.closed.IsSet() { + if mc.closed.Load() { if err := mc.canceled.Value(); err != nil { return err } @@ -162,7 +162,7 @@ func (mc *mysqlConn) error() error { } func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) { - if mc.closed.IsSet() { + if mc.closed.Load() { errLog.Print(ErrInvalidConn) return nil, driver.ErrBadConn } @@ -295,7 +295,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin } func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) { - if mc.closed.IsSet() { + if mc.closed.Load() { errLog.Print(ErrInvalidConn) return nil, driver.ErrBadConn } @@ -356,7 +356,7 @@ func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, erro } func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) { - if mc.closed.IsSet() { + if mc.closed.Load() { errLog.Print(ErrInvalidConn) return nil, driver.ErrBadConn } @@ -450,7 +450,7 @@ func (mc *mysqlConn) finish() { // Ping implements driver.Pinger interface func (mc *mysqlConn) Ping(ctx context.Context) (err error) { - if mc.closed.IsSet() { + if mc.closed.Load() { errLog.Print(ErrInvalidConn) return driver.ErrBadConn } @@ -469,7 +469,7 @@ func (mc *mysqlConn) Ping(ctx context.Context) (err error) { // BeginTx implements driver.ConnBeginTx interface func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { - if mc.closed.IsSet() { + if mc.closed.Load() { return nil, driver.ErrBadConn } @@ -636,7 +636,7 @@ func (mc *mysqlConn) CheckNamedValue(nv *driver.NamedValue) (err error) { // ResetSession implements driver.SessionResetter. // (From Go 1.10) func (mc *mysqlConn) ResetSession(ctx context.Context) error { - if mc.closed.IsSet() { + if mc.closed.Load() { return driver.ErrBadConn } mc.reset = true @@ -646,5 +646,5 @@ func (mc *mysqlConn) ResetSession(ctx context.Context) error { // IsValid implements driver.Validator interface // (From Go 1.15) func (mc *mysqlConn) IsValid() bool { - return !mc.closed.IsSet() + return !mc.closed.Load() } diff --git a/vendor/github.com/go-sql-driver/mysql/const.go b/vendor/github.com/go-sql-driver/mysql/const.go index b1e6b85e..64e2bced 100644 --- a/vendor/github.com/go-sql-driver/mysql/const.go +++ b/vendor/github.com/go-sql-driver/mysql/const.go @@ -10,7 +10,7 @@ package mysql const ( defaultAuthPlugin = "mysql_native_password" - defaultMaxAllowedPacket = 4 << 20 // 4 MiB + defaultMaxAllowedPacket = 64 << 20 // 64 MiB. See https://github.com/go-sql-driver/mysql/issues/1355 minProtocolVersion = 10 maxPacketSize = 1<<24 - 1 timeFormat = "2006-01-02 15:04:05.999999" diff --git a/vendor/github.com/go-sql-driver/mysql/driver.go b/vendor/github.com/go-sql-driver/mysql/driver.go index c1bdf119..ad7aec21 100644 --- a/vendor/github.com/go-sql-driver/mysql/driver.go +++ b/vendor/github.com/go-sql-driver/mysql/driver.go @@ -8,10 +8,10 @@ // // The driver should be used via the database/sql package: // -// import "database/sql" -// import _ "github.com/go-sql-driver/mysql" +// import "database/sql" +// import _ "github.com/go-sql-driver/mysql" // -// db, err := sql.Open("mysql", "user:password@/dbname") +// db, err := sql.Open("mysql", "user:password@/dbname") // // See https://github.com/go-sql-driver/mysql#usage for details package mysql diff --git a/vendor/github.com/go-sql-driver/mysql/dsn.go b/vendor/github.com/go-sql-driver/mysql/dsn.go index 93f3548c..4b71aaab 100644 --- a/vendor/github.com/go-sql-driver/mysql/dsn.go +++ b/vendor/github.com/go-sql-driver/mysql/dsn.go @@ -46,22 +46,23 @@ type Config struct { ServerPubKey string // Server public key name pubKey *rsa.PublicKey // Server public key TLSConfig string // TLS configuration name - tls *tls.Config // TLS configuration + TLS *tls.Config // TLS configuration, its priority is higher than TLSConfig Timeout time.Duration // Dial timeout ReadTimeout time.Duration // I/O read timeout WriteTimeout time.Duration // I/O write timeout - AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE - AllowCleartextPasswords bool // Allows the cleartext client side plugin - AllowNativePasswords bool // Allows the native password authentication method - AllowOldPasswords bool // Allows the old insecure password method - CheckConnLiveness bool // Check connections for liveness before using them - ClientFoundRows bool // Return number of matching rows instead of rows changed - ColumnsWithAlias bool // Prepend table alias to column names - InterpolateParams bool // Interpolate placeholders into query string - MultiStatements bool // Allow multiple statements in one query - ParseTime bool // Parse time values to time.Time - RejectReadOnly bool // Reject read-only connections + AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE + AllowCleartextPasswords bool // Allows the cleartext client side plugin + AllowFallbackToPlaintext bool // Allows fallback to unencrypted connection if server does not support TLS + AllowNativePasswords bool // Allows the native password authentication method + AllowOldPasswords bool // Allows the old insecure password method + CheckConnLiveness bool // Check connections for liveness before using them + ClientFoundRows bool // Return number of matching rows instead of rows changed + ColumnsWithAlias bool // Prepend table alias to column names + InterpolateParams bool // Interpolate placeholders into query string + MultiStatements bool // Allow multiple statements in one query + ParseTime bool // Parse time values to time.Time + RejectReadOnly bool // Reject read-only connections } // NewConfig creates a new Config and sets default values. @@ -77,8 +78,8 @@ func NewConfig() *Config { func (cfg *Config) Clone() *Config { cp := *cfg - if cp.tls != nil { - cp.tls = cfg.tls.Clone() + if cp.TLS != nil { + cp.TLS = cfg.TLS.Clone() } if len(cp.Params) > 0 { cp.Params = make(map[string]string, len(cfg.Params)) @@ -119,24 +120,29 @@ func (cfg *Config) normalize() error { cfg.Addr = ensureHavePort(cfg.Addr) } - switch cfg.TLSConfig { - case "false", "": - // don't set anything - case "true": - cfg.tls = &tls.Config{} - case "skip-verify", "preferred": - cfg.tls = &tls.Config{InsecureSkipVerify: true} - default: - cfg.tls = getTLSConfigClone(cfg.TLSConfig) - if cfg.tls == nil { - return errors.New("invalid value / unknown config name: " + cfg.TLSConfig) + if cfg.TLS == nil { + switch cfg.TLSConfig { + case "false", "": + // don't set anything + case "true": + cfg.TLS = &tls.Config{} + case "skip-verify": + cfg.TLS = &tls.Config{InsecureSkipVerify: true} + case "preferred": + cfg.TLS = &tls.Config{InsecureSkipVerify: true} + cfg.AllowFallbackToPlaintext = true + default: + cfg.TLS = getTLSConfigClone(cfg.TLSConfig) + if cfg.TLS == nil { + return errors.New("invalid value / unknown config name: " + cfg.TLSConfig) + } } } - if cfg.tls != nil && cfg.tls.ServerName == "" && !cfg.tls.InsecureSkipVerify { + if cfg.TLS != nil && cfg.TLS.ServerName == "" && !cfg.TLS.InsecureSkipVerify { host, _, err := net.SplitHostPort(cfg.Addr) if err == nil { - cfg.tls.ServerName = host + cfg.TLS.ServerName = host } } @@ -204,6 +210,10 @@ func (cfg *Config) FormatDSN() string { writeDSNParam(&buf, &hasParam, "allowCleartextPasswords", "true") } + if cfg.AllowFallbackToPlaintext { + writeDSNParam(&buf, &hasParam, "allowFallbackToPlaintext", "true") + } + if !cfg.AllowNativePasswords { writeDSNParam(&buf, &hasParam, "allowNativePasswords", "false") } @@ -391,6 +401,14 @@ func parseDSNParams(cfg *Config, params string) (err error) { return errors.New("invalid bool value: " + value) } + // Allow fallback to unencrypted connection if server does not support TLS + case "allowFallbackToPlaintext": + var isBool bool + cfg.AllowFallbackToPlaintext, isBool = readBool(value) + if !isBool { + return errors.New("invalid bool value: " + value) + } + // Use native password authentication case "allowNativePasswords": var isBool bool @@ -426,7 +444,6 @@ func parseDSNParams(cfg *Config, params string) (err error) { // Collation case "collation": cfg.Collation = value - break case "columnsWithAlias": var isBool bool diff --git a/vendor/github.com/go-sql-driver/mysql/errors.go b/vendor/github.com/go-sql-driver/mysql/errors.go index 760782ff..ff9a8f08 100644 --- a/vendor/github.com/go-sql-driver/mysql/errors.go +++ b/vendor/github.com/go-sql-driver/mysql/errors.go @@ -27,7 +27,7 @@ var ( ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+") ErrPktSync = errors.New("commands out of sync. You can't run this command now") ErrPktSyncMul = errors.New("commands out of sync. Did you run multiple statements at once?") - ErrPktTooLarge = errors.New("packet for query is too large. Try adjusting the 'max_allowed_packet' variable on the server") + ErrPktTooLarge = errors.New("packet for query is too large. Try adjusting the `Config.MaxAllowedPacket`") ErrBusyBuffer = errors.New("busy buffer") // errBadConnNoWrite is used for connection errors where nothing was sent to the database yet. @@ -56,10 +56,22 @@ func SetLogger(logger Logger) error { // MySQLError is an error type which represents a single MySQL error type MySQLError struct { - Number uint16 - Message string + Number uint16 + SQLState [5]byte + Message string } func (me *MySQLError) Error() string { + if me.SQLState != [5]byte{} { + return fmt.Sprintf("Error %d (%s): %s", me.Number, me.SQLState, me.Message) + } + return fmt.Sprintf("Error %d: %s", me.Number, me.Message) } + +func (me *MySQLError) Is(err error) bool { + if merr, ok := err.(*MySQLError); ok { + return merr.Number == me.Number + } + return false +} diff --git a/vendor/github.com/go-sql-driver/mysql/fields.go b/vendor/github.com/go-sql-driver/mysql/fields.go index ed6c7a37..e0654a83 100644 --- a/vendor/github.com/go-sql-driver/mysql/fields.go +++ b/vendor/github.com/go-sql-driver/mysql/fields.go @@ -41,6 +41,9 @@ func (mf *mysqlField) typeDatabaseName() string { case fieldTypeJSON: return "JSON" case fieldTypeLong: + if mf.flags&flagUnsigned != 0 { + return "UNSIGNED INT" + } return "INT" case fieldTypeLongBLOB: if mf.charSet != collations[binaryCollation] { @@ -48,6 +51,9 @@ func (mf *mysqlField) typeDatabaseName() string { } return "LONGBLOB" case fieldTypeLongLong: + if mf.flags&flagUnsigned != 0 { + return "UNSIGNED BIGINT" + } return "BIGINT" case fieldTypeMediumBLOB: if mf.charSet != collations[binaryCollation] { @@ -63,6 +69,9 @@ func (mf *mysqlField) typeDatabaseName() string { case fieldTypeSet: return "SET" case fieldTypeShort: + if mf.flags&flagUnsigned != 0 { + return "UNSIGNED SMALLINT" + } return "SMALLINT" case fieldTypeString: if mf.charSet == collations[binaryCollation] { @@ -74,6 +83,9 @@ func (mf *mysqlField) typeDatabaseName() string { case fieldTypeTimestamp: return "TIMESTAMP" case fieldTypeTiny: + if mf.flags&flagUnsigned != 0 { + return "UNSIGNED TINYINT" + } return "TINYINT" case fieldTypeTinyBLOB: if mf.charSet != collations[binaryCollation] { @@ -106,7 +118,7 @@ var ( scanTypeInt64 = reflect.TypeOf(int64(0)) scanTypeNullFloat = reflect.TypeOf(sql.NullFloat64{}) scanTypeNullInt = reflect.TypeOf(sql.NullInt64{}) - scanTypeNullTime = reflect.TypeOf(nullTime{}) + scanTypeNullTime = reflect.TypeOf(sql.NullTime{}) scanTypeUint8 = reflect.TypeOf(uint8(0)) scanTypeUint16 = reflect.TypeOf(uint16(0)) scanTypeUint32 = reflect.TypeOf(uint32(0)) diff --git a/vendor/github.com/go-sql-driver/mysql/fuzz.go b/vendor/github.com/go-sql-driver/mysql/fuzz.go index fa75adf6..3a4ec25a 100644 --- a/vendor/github.com/go-sql-driver/mysql/fuzz.go +++ b/vendor/github.com/go-sql-driver/mysql/fuzz.go @@ -6,6 +6,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at http://mozilla.org/MPL/2.0/. +//go:build gofuzz // +build gofuzz package mysql diff --git a/vendor/github.com/go-sql-driver/mysql/infile.go b/vendor/github.com/go-sql-driver/mysql/infile.go index 60effdfc..3279dcff 100644 --- a/vendor/github.com/go-sql-driver/mysql/infile.go +++ b/vendor/github.com/go-sql-driver/mysql/infile.go @@ -28,12 +28,11 @@ var ( // Alternatively you can allow the use of all local files with // the DSN parameter 'allowAllFiles=true' // -// filePath := "/home/gopher/data.csv" -// mysql.RegisterLocalFile(filePath) -// err := db.Exec("LOAD DATA LOCAL INFILE '" + filePath + "' INTO TABLE foo") -// if err != nil { -// ... -// +// filePath := "/home/gopher/data.csv" +// mysql.RegisterLocalFile(filePath) +// err := db.Exec("LOAD DATA LOCAL INFILE '" + filePath + "' INTO TABLE foo") +// if err != nil { +// ... func RegisterLocalFile(filePath string) { fileRegisterLock.Lock() // lazy map init @@ -58,15 +57,14 @@ func DeregisterLocalFile(filePath string) { // If the handler returns a io.ReadCloser Close() is called when the // request is finished. // -// mysql.RegisterReaderHandler("data", func() io.Reader { -// var csvReader io.Reader // Some Reader that returns CSV data -// ... // Open Reader here -// return csvReader -// }) -// err := db.Exec("LOAD DATA LOCAL INFILE 'Reader::data' INTO TABLE foo") -// if err != nil { -// ... -// +// mysql.RegisterReaderHandler("data", func() io.Reader { +// var csvReader io.Reader // Some Reader that returns CSV data +// ... // Open Reader here +// return csvReader +// }) +// err := db.Exec("LOAD DATA LOCAL INFILE 'Reader::data' INTO TABLE foo") +// if err != nil { +// ... func RegisterReaderHandler(name string, handler func() io.Reader) { readerRegisterLock.Lock() // lazy map init @@ -93,10 +91,12 @@ func deferredClose(err *error, closer io.Closer) { } } +const defaultPacketSize = 16 * 1024 // 16KB is small enough for disk readahead and large enough for TCP + func (mc *mysqlConn) handleInFileRequest(name string) (err error) { var rdr io.Reader var data []byte - packetSize := 16 * 1024 // 16KB is small enough for disk readahead and large enough for TCP + packetSize := defaultPacketSize if mc.maxWriteSize < packetSize { packetSize = mc.maxWriteSize } diff --git a/vendor/github.com/go-sql-driver/mysql/nulltime.go b/vendor/github.com/go-sql-driver/mysql/nulltime.go index 651723a9..36c8a42c 100644 --- a/vendor/github.com/go-sql-driver/mysql/nulltime.go +++ b/vendor/github.com/go-sql-driver/mysql/nulltime.go @@ -9,11 +9,32 @@ package mysql import ( + "database/sql" "database/sql/driver" "fmt" "time" ) +// NullTime represents a time.Time that may be NULL. +// NullTime implements the Scanner interface so +// it can be used as a scan destination: +// +// var nt NullTime +// err := db.QueryRow("SELECT time FROM foo WHERE id=?", id).Scan(&nt) +// ... +// if nt.Valid { +// // use nt.Time +// } else { +// // NULL value +// } +// +// # This NullTime implementation is not driver-specific +// +// Deprecated: NullTime doesn't honor the loc DSN parameter. +// NullTime.Scan interprets a time as UTC, not the loc DSN parameter. +// Use sql.NullTime instead. +type NullTime sql.NullTime + // Scan implements the Scanner interface. // The value type must be time.Time or string / []byte (formatted time-string), // otherwise Scan fails. diff --git a/vendor/github.com/go-sql-driver/mysql/nulltime_go113.go b/vendor/github.com/go-sql-driver/mysql/nulltime_go113.go deleted file mode 100644 index 453b4b39..00000000 --- a/vendor/github.com/go-sql-driver/mysql/nulltime_go113.go +++ /dev/null @@ -1,40 +0,0 @@ -// Go MySQL Driver - A MySQL-Driver for Go's database/sql package -// -// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at http://mozilla.org/MPL/2.0/. - -// +build go1.13 - -package mysql - -import ( - "database/sql" -) - -// NullTime represents a time.Time that may be NULL. -// NullTime implements the Scanner interface so -// it can be used as a scan destination: -// -// var nt NullTime -// err := db.QueryRow("SELECT time FROM foo WHERE id=?", id).Scan(&nt) -// ... -// if nt.Valid { -// // use nt.Time -// } else { -// // NULL value -// } -// -// This NullTime implementation is not driver-specific -// -// Deprecated: NullTime doesn't honor the loc DSN parameter. -// NullTime.Scan interprets a time as UTC, not the loc DSN parameter. -// Use sql.NullTime instead. -type NullTime sql.NullTime - -// for internal use. -// the mysql package uses sql.NullTime if it is available. -// if not, the package uses mysql.NullTime. -type nullTime = sql.NullTime // sql.NullTime is available diff --git a/vendor/github.com/go-sql-driver/mysql/nulltime_legacy.go b/vendor/github.com/go-sql-driver/mysql/nulltime_legacy.go deleted file mode 100644 index 9f7ae27a..00000000 --- a/vendor/github.com/go-sql-driver/mysql/nulltime_legacy.go +++ /dev/null @@ -1,39 +0,0 @@ -// Go MySQL Driver - A MySQL-Driver for Go's database/sql package -// -// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at http://mozilla.org/MPL/2.0/. - -// +build !go1.13 - -package mysql - -import ( - "time" -) - -// NullTime represents a time.Time that may be NULL. -// NullTime implements the Scanner interface so -// it can be used as a scan destination: -// -// var nt NullTime -// err := db.QueryRow("SELECT time FROM foo WHERE id=?", id).Scan(&nt) -// ... -// if nt.Valid { -// // use nt.Time -// } else { -// // NULL value -// } -// -// This NullTime implementation is not driver-specific -type NullTime struct { - Time time.Time - Valid bool // Valid is true if Time is not NULL -} - -// for internal use. -// the mysql package uses sql.NullTime if it is available. -// if not, the package uses mysql.NullTime. -type nullTime = NullTime // sql.NullTime is not available diff --git a/vendor/github.com/go-sql-driver/mysql/packets.go b/vendor/github.com/go-sql-driver/mysql/packets.go index 6664e5ae..ee05c95a 100644 --- a/vendor/github.com/go-sql-driver/mysql/packets.go +++ b/vendor/github.com/go-sql-driver/mysql/packets.go @@ -110,14 +110,13 @@ func (mc *mysqlConn) writePacket(data []byte) error { conn = mc.rawConn } var err error - // If this connection has a ReadTimeout which we've been setting on - // reads, reset it to its default value before we attempt a non-blocking - // read, otherwise the scheduler will just time us out before we can read - if mc.cfg.ReadTimeout != 0 { - err = conn.SetReadDeadline(time.Time{}) - } - if err == nil && mc.cfg.CheckConnLiveness { - err = connCheck(conn) + if mc.cfg.CheckConnLiveness { + if mc.cfg.ReadTimeout != 0 { + err = conn.SetReadDeadline(time.Now().Add(mc.cfg.ReadTimeout)) + } + if err == nil { + err = connCheck(conn) + } } if err != nil { errLog.Print("closing bad idle connection: ", err) @@ -223,9 +222,9 @@ func (mc *mysqlConn) readHandshakePacket() (data []byte, plugin string, err erro if mc.flags&clientProtocol41 == 0 { return nil, "", ErrOldProtocol } - if mc.flags&clientSSL == 0 && mc.cfg.tls != nil { - if mc.cfg.TLSConfig == "preferred" { - mc.cfg.tls = nil + if mc.flags&clientSSL == 0 && mc.cfg.TLS != nil { + if mc.cfg.AllowFallbackToPlaintext { + mc.cfg.TLS = nil } else { return nil, "", ErrNoTLS } @@ -293,7 +292,7 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string } // To enable TLS / SSL - if mc.cfg.tls != nil { + if mc.cfg.TLS != nil { clientFlags |= clientSSL } @@ -357,14 +356,14 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string // SSL Connection Request Packet // http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::SSLRequest - if mc.cfg.tls != nil { + if mc.cfg.TLS != nil { // Send TLS / SSL request packet if err := mc.writePacket(data[:(4+4+1+23)+4]); err != nil { return err } // Switch to TLS - tlsConn := tls.Client(mc.netConn, mc.cfg.tls) + tlsConn := tls.Client(mc.netConn, mc.cfg.TLS) if err := tlsConn.Handshake(); err != nil { return err } @@ -588,19 +587,20 @@ func (mc *mysqlConn) handleErrorPacket(data []byte) error { return driver.ErrBadConn } + me := &MySQLError{Number: errno} + pos := 3 // SQL State [optional: # + 5bytes string] if data[3] == 0x23 { - //sqlstate := string(data[4 : 4+5]) + copy(me.SQLState[:], data[4:4+5]) pos = 9 } // Error Message [string] - return &MySQLError{ - Number: errno, - Message: string(data[pos:]), - } + me.Message = string(data[pos:]) + + return me } func readStatus(b []byte) statusFlag { @@ -761,40 +761,40 @@ func (rows *textRows) readRow(dest []driver.Value) error { } // RowSet Packet - var n int - var isNull bool - pos := 0 + var ( + n int + isNull bool + pos int = 0 + ) for i := range dest { // Read bytes and convert to string dest[i], isNull, n, err = readLengthEncodedString(data[pos:]) pos += n - if err == nil { - if !isNull { - if !mc.parseTime { - continue - } else { - switch rows.rs.columns[i].fieldType { - case fieldTypeTimestamp, fieldTypeDateTime, - fieldTypeDate, fieldTypeNewDate: - dest[i], err = parseDateTime( - dest[i].([]byte), - mc.cfg.Loc, - ) - if err == nil { - continue - } - default: - continue - } - } - } else { - dest[i] = nil - continue + if err != nil { + return err + } + + if isNull { + dest[i] = nil + continue + } + + if !mc.parseTime { + continue + } + + // Parse time field + switch rows.rs.columns[i].fieldType { + case fieldTypeTimestamp, + fieldTypeDateTime, + fieldTypeDate, + fieldTypeNewDate: + if dest[i], err = parseDateTime(dest[i].([]byte), mc.cfg.Loc); err != nil { + return err } } - return err // err != nil } return nil diff --git a/vendor/github.com/go-sql-driver/mysql/statement.go b/vendor/github.com/go-sql-driver/mysql/statement.go index 18a3ae49..10ece8bd 100644 --- a/vendor/github.com/go-sql-driver/mysql/statement.go +++ b/vendor/github.com/go-sql-driver/mysql/statement.go @@ -23,7 +23,7 @@ type mysqlStmt struct { } func (stmt *mysqlStmt) Close() error { - if stmt.mc == nil || stmt.mc.closed.IsSet() { + if stmt.mc == nil || stmt.mc.closed.Load() { // driver.Stmt.Close can be called more than once, thus this function // has to be idempotent. // See also Issue #450 and golang/go#16019. @@ -50,7 +50,7 @@ func (stmt *mysqlStmt) CheckNamedValue(nv *driver.NamedValue) (err error) { } func (stmt *mysqlStmt) Exec(args []driver.Value) (driver.Result, error) { - if stmt.mc.closed.IsSet() { + if stmt.mc.closed.Load() { errLog.Print(ErrInvalidConn) return nil, driver.ErrBadConn } @@ -98,7 +98,7 @@ func (stmt *mysqlStmt) Query(args []driver.Value) (driver.Rows, error) { } func (stmt *mysqlStmt) query(args []driver.Value) (*binaryRows, error) { - if stmt.mc.closed.IsSet() { + if stmt.mc.closed.Load() { errLog.Print(ErrInvalidConn) return nil, driver.ErrBadConn } @@ -157,7 +157,7 @@ func (c converter) ConvertValue(v interface{}) (driver.Value, error) { if driver.IsValue(sv) { return sv, nil } - // A value returend from the Valuer interface can be "a type handled by + // A value returned from the Valuer interface can be "a type handled by // a database driver's NamedValueChecker interface" so we should accept // uint64 here as well. if u, ok := sv.(uint64); ok { diff --git a/vendor/github.com/go-sql-driver/mysql/transaction.go b/vendor/github.com/go-sql-driver/mysql/transaction.go index 417d7279..4a4b6100 100644 --- a/vendor/github.com/go-sql-driver/mysql/transaction.go +++ b/vendor/github.com/go-sql-driver/mysql/transaction.go @@ -13,7 +13,7 @@ type mysqlTx struct { } func (tx *mysqlTx) Commit() (err error) { - if tx.mc == nil || tx.mc.closed.IsSet() { + if tx.mc == nil || tx.mc.closed.Load() { return ErrInvalidConn } err = tx.mc.exec("COMMIT") @@ -22,7 +22,7 @@ func (tx *mysqlTx) Commit() (err error) { } func (tx *mysqlTx) Rollback() (err error) { - if tx.mc == nil || tx.mc.closed.IsSet() { + if tx.mc == nil || tx.mc.closed.Load() { return ErrInvalidConn } err = tx.mc.exec("ROLLBACK") diff --git a/vendor/github.com/go-sql-driver/mysql/utils.go b/vendor/github.com/go-sql-driver/mysql/utils.go index d6545f5b..15dbd8d1 100644 --- a/vendor/github.com/go-sql-driver/mysql/utils.go +++ b/vendor/github.com/go-sql-driver/mysql/utils.go @@ -35,26 +35,25 @@ var ( // Note: The provided tls.Config is exclusively owned by the driver after // registering it. // -// rootCertPool := x509.NewCertPool() -// pem, err := ioutil.ReadFile("/path/ca-cert.pem") -// if err != nil { -// log.Fatal(err) -// } -// if ok := rootCertPool.AppendCertsFromPEM(pem); !ok { -// log.Fatal("Failed to append PEM.") -// } -// clientCert := make([]tls.Certificate, 0, 1) -// certs, err := tls.LoadX509KeyPair("/path/client-cert.pem", "/path/client-key.pem") -// if err != nil { -// log.Fatal(err) -// } -// clientCert = append(clientCert, certs) -// mysql.RegisterTLSConfig("custom", &tls.Config{ -// RootCAs: rootCertPool, -// Certificates: clientCert, -// }) -// db, err := sql.Open("mysql", "user@tcp(localhost:3306)/test?tls=custom") -// +// rootCertPool := x509.NewCertPool() +// pem, err := ioutil.ReadFile("/path/ca-cert.pem") +// if err != nil { +// log.Fatal(err) +// } +// if ok := rootCertPool.AppendCertsFromPEM(pem); !ok { +// log.Fatal("Failed to append PEM.") +// } +// clientCert := make([]tls.Certificate, 0, 1) +// certs, err := tls.LoadX509KeyPair("/path/client-cert.pem", "/path/client-key.pem") +// if err != nil { +// log.Fatal(err) +// } +// clientCert = append(clientCert, certs) +// mysql.RegisterTLSConfig("custom", &tls.Config{ +// RootCAs: rootCertPool, +// Certificates: clientCert, +// }) +// db, err := sql.Open("mysql", "user@tcp(localhost:3306)/test?tls=custom") func RegisterTLSConfig(key string, config *tls.Config) error { if _, isBool := readBool(key); isBool || strings.ToLower(key) == "skip-verify" || strings.ToLower(key) == "preferred" { return fmt.Errorf("key '%s' is reserved", key) @@ -118,10 +117,6 @@ func parseDateTime(b []byte, loc *time.Location) (time.Time, error) { if err != nil { return time.Time{}, err } - if year <= 0 { - year = 1 - } - if b[4] != '-' { return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[4]) } @@ -130,9 +125,6 @@ func parseDateTime(b []byte, loc *time.Location) (time.Time, error) { if err != nil { return time.Time{}, err } - if m <= 0 { - m = 1 - } month := time.Month(m) if b[7] != '-' { @@ -143,9 +135,6 @@ func parseDateTime(b []byte, loc *time.Location) (time.Time, error) { if err != nil { return time.Time{}, err } - if day <= 0 { - day = 1 - } if len(b) == 10 { return time.Date(year, month, day, 0, 0, 0, 0, loc), nil } @@ -199,7 +188,7 @@ func parseByteYear(b []byte) (int, error) { return 0, err } year += v * n - n = n / 10 + n /= 10 } return year, nil } @@ -542,7 +531,7 @@ func stringToInt(b []byte) int { return val } -// returns the string read as a bytes slice, wheter the value is NULL, +// returns the string read as a bytes slice, whether the value is NULL, // the number of bytes read and an error, in case the string is longer than // the input slice func readLengthEncodedString(b []byte) ([]byte, bool, int, error) { @@ -652,32 +641,32 @@ func escapeBytesBackslash(buf, v []byte) []byte { for _, c := range v { switch c { case '\x00': - buf[pos] = '\\' buf[pos+1] = '0' + buf[pos] = '\\' pos += 2 case '\n': - buf[pos] = '\\' buf[pos+1] = 'n' + buf[pos] = '\\' pos += 2 case '\r': - buf[pos] = '\\' buf[pos+1] = 'r' + buf[pos] = '\\' pos += 2 case '\x1a': - buf[pos] = '\\' buf[pos+1] = 'Z' + buf[pos] = '\\' pos += 2 case '\'': - buf[pos] = '\\' buf[pos+1] = '\'' + buf[pos] = '\\' pos += 2 case '"': - buf[pos] = '\\' buf[pos+1] = '"' + buf[pos] = '\\' pos += 2 case '\\': - buf[pos] = '\\' buf[pos+1] = '\\' + buf[pos] = '\\' pos += 2 default: buf[pos] = c @@ -697,32 +686,32 @@ func escapeStringBackslash(buf []byte, v string) []byte { c := v[i] switch c { case '\x00': - buf[pos] = '\\' buf[pos+1] = '0' + buf[pos] = '\\' pos += 2 case '\n': - buf[pos] = '\\' buf[pos+1] = 'n' + buf[pos] = '\\' pos += 2 case '\r': - buf[pos] = '\\' buf[pos+1] = 'r' + buf[pos] = '\\' pos += 2 case '\x1a': - buf[pos] = '\\' buf[pos+1] = 'Z' + buf[pos] = '\\' pos += 2 case '\'': - buf[pos] = '\\' buf[pos+1] = '\'' + buf[pos] = '\\' pos += 2 case '"': - buf[pos] = '\\' buf[pos+1] = '"' + buf[pos] = '\\' pos += 2 case '\\': - buf[pos] = '\\' buf[pos+1] = '\\' + buf[pos] = '\\' pos += 2 default: buf[pos] = c @@ -744,8 +733,8 @@ func escapeBytesQuotes(buf, v []byte) []byte { for _, c := range v { if c == '\'' { - buf[pos] = '\'' buf[pos+1] = '\'' + buf[pos] = '\'' pos += 2 } else { buf[pos] = c @@ -764,8 +753,8 @@ func escapeStringQuotes(buf []byte, v string) []byte { for i := 0; i < len(v); i++ { c := v[i] if c == '\'' { - buf[pos] = '\'' buf[pos+1] = '\'' + buf[pos] = '\'' pos += 2 } else { buf[pos] = c @@ -790,39 +779,16 @@ type noCopy struct{} // Lock is a no-op used by -copylocks checker from `go vet`. func (*noCopy) Lock() {} -// atomicBool is a wrapper around uint32 for usage as a boolean value with -// atomic access. -type atomicBool struct { - _noCopy noCopy - value uint32 -} - -// IsSet returns whether the current boolean value is true -func (ab *atomicBool) IsSet() bool { - return atomic.LoadUint32(&ab.value) > 0 -} - -// Set sets the value of the bool regardless of the previous value -func (ab *atomicBool) Set(value bool) { - if value { - atomic.StoreUint32(&ab.value, 1) - } else { - atomic.StoreUint32(&ab.value, 0) - } -} - -// TrySet sets the value of the bool and returns whether the value changed -func (ab *atomicBool) TrySet(value bool) bool { - if value { - return atomic.SwapUint32(&ab.value, 1) == 0 - } - return atomic.SwapUint32(&ab.value, 0) > 0 -} +// Unlock is a no-op used by -copylocks checker from `go vet`. +// noCopy should implement sync.Locker from Go 1.11 +// https://github.com/golang/go/commit/c2eba53e7f80df21d51285879d51ab81bcfbf6bc +// https://github.com/golang/go/issues/26165 +func (*noCopy) Unlock() {} // atomicError is a wrapper for atomically accessed error values type atomicError struct { - _noCopy noCopy - value atomic.Value + _ noCopy + value atomic.Value } // Set sets the error value regardless of the previous value. diff --git a/vendor/github.com/golang-jwt/jwt/v4/.gitignore b/vendor/github.com/golang-jwt/jwt/v4/.gitignore new file mode 100644 index 00000000..09573e01 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +bin +.idea/ + diff --git a/vendor/github.com/golang-jwt/jwt/v4/LICENSE b/vendor/github.com/golang-jwt/jwt/v4/LICENSE new file mode 100644 index 00000000..35dbc252 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) 2012 Dave Grijalva +Copyright (c) 2021 golang-jwt maintainers + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/vendor/github.com/golang-jwt/jwt/v4/MIGRATION_GUIDE.md b/vendor/github.com/golang-jwt/jwt/v4/MIGRATION_GUIDE.md new file mode 100644 index 00000000..32966f59 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/MIGRATION_GUIDE.md @@ -0,0 +1,22 @@ +## Migration Guide (v4.0.0) + +Starting from [v4.0.0](https://github.com/golang-jwt/jwt/releases/tag/v4.0.0), the import path will be: + + "github.com/golang-jwt/jwt/v4" + +The `/v4` version will be backwards compatible with existing `v3.x.y` tags in this repo, as well as +`github.com/dgrijalva/jwt-go`. For most users this should be a drop-in replacement, if you're having +troubles migrating, please open an issue. + +You can replace all occurrences of `github.com/dgrijalva/jwt-go` or `github.com/golang-jwt/jwt` with `github.com/golang-jwt/jwt/v4`, either manually or by using tools such as `sed` or `gofmt`. + +And then you'd typically run: + +``` +go get github.com/golang-jwt/jwt/v4 +go mod tidy +``` + +## Older releases (before v3.2.0) + +The original migration guide for older releases can be found at https://github.com/dgrijalva/jwt-go/blob/master/MIGRATION_GUIDE.md. diff --git a/vendor/github.com/golang-jwt/jwt/v4/README.md b/vendor/github.com/golang-jwt/jwt/v4/README.md new file mode 100644 index 00000000..30f2f2a6 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/README.md @@ -0,0 +1,138 @@ +# jwt-go + +[![build](https://github.com/golang-jwt/jwt/actions/workflows/build.yml/badge.svg)](https://github.com/golang-jwt/jwt/actions/workflows/build.yml) +[![Go Reference](https://pkg.go.dev/badge/github.com/golang-jwt/jwt/v4.svg)](https://pkg.go.dev/github.com/golang-jwt/jwt/v4) + +A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](https://datatracker.ietf.org/doc/html/rfc7519). + +Starting with [v4.0.0](https://github.com/golang-jwt/jwt/releases/tag/v4.0.0) this project adds Go module support, but maintains backwards compatibility with older `v3.x.y` tags and upstream `github.com/dgrijalva/jwt-go`. +See the [`MIGRATION_GUIDE.md`](./MIGRATION_GUIDE.md) for more information. + +> After the original author of the library suggested migrating the maintenance of `jwt-go`, a dedicated team of open source maintainers decided to clone the existing library into this repository. See [dgrijalva/jwt-go#462](https://github.com/dgrijalva/jwt-go/issues/462) for a detailed discussion on this topic. + + +**SECURITY NOTICE:** Some older versions of Go have a security issue in the crypto/elliptic. Recommendation is to upgrade to at least 1.15 See issue [dgrijalva/jwt-go#216](https://github.com/dgrijalva/jwt-go/issues/216) for more detail. + +**SECURITY NOTICE:** It's important that you [validate the `alg` presented is what you expect](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). This library attempts to make it easy to do the right thing by requiring key types match the expected alg, but you should take the extra step to verify it in your usage. See the examples provided. + +### Supported Go versions + +Our support of Go versions is aligned with Go's [version release policy](https://golang.org/doc/devel/release#policy). +So we will support a major version of Go until there are two newer major releases. +We no longer support building jwt-go with unsupported Go versions, as these contain security vulnerabilities +which will not be fixed. + +## What the heck is a JWT? + +JWT.io has [a great introduction](https://jwt.io/introduction) to JSON Web Tokens. + +In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](https://datatracker.ietf.org/doc/html/rfc4648) encoded. The last part is the signature, encoded the same way. + +The first part is called the header. It contains the necessary information for verifying the last part, the signature. For example, which encryption method was used for signing and what key was used. + +The part in the middle is the interesting bit. It's called the Claims and contains the actual stuff you care about. Refer to [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519) for information about reserved keys and the proper way to add your own. + +## What's in the box? + +This library supports the parsing and verification as well as the generation and signing of JWTs. Current supported signing algorithms are HMAC SHA, RSA, RSA-PSS, and ECDSA, though hooks are present for adding your own. + +## Installation Guidelines + +1. To install the jwt package, you first need to have [Go](https://go.dev/doc/install) installed, then you can use the command below to add `jwt-go` as a dependency in your Go program. + +```sh +go get -u github.com/golang-jwt/jwt/v4 +``` + +2. Import it in your code: + +```go +import "github.com/golang-jwt/jwt/v4" +``` + +## Examples + +See [the project documentation](https://pkg.go.dev/github.com/golang-jwt/jwt/v4) for examples of usage: + +* [Simple example of parsing and validating a token](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#example-Parse-Hmac) +* [Simple example of building and signing a token](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#example-New-Hmac) +* [Directory of Examples](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#pkg-examples) + +## Extensions + +This library publishes all the necessary components for adding your own signing methods or key functions. Simply implement the `SigningMethod` interface and register a factory method using `RegisterSigningMethod` or provide a `jwt.Keyfunc`. + +A common use case would be integrating with different 3rd party signature providers, like key management services from various cloud providers or Hardware Security Modules (HSMs) or to implement additional standards. + +| Extension | Purpose | Repo | +| --------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------ | +| GCP | Integrates with multiple Google Cloud Platform signing tools (AppEngine, IAM API, Cloud KMS) | https://github.com/someone1/gcp-jwt-go | +| AWS | Integrates with AWS Key Management Service, KMS | https://github.com/matelang/jwt-go-aws-kms | +| JWKS | Provides support for JWKS ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) as a `jwt.Keyfunc` | https://github.com/MicahParks/keyfunc | + +*Disclaimer*: Unless otherwise specified, these integrations are maintained by third parties and should not be considered as a primary offer by any of the mentioned cloud providers + +## Compliance + +This library was last reviewed to comply with [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519) dated May 2015 with a few notable differences: + +* In order to protect against accidental use of [Unsecured JWTs](https://datatracker.ietf.org/doc/html/rfc7519#section-6), tokens using `alg=none` will only be accepted if the constant `jwt.UnsafeAllowNoneSignatureType` is provided as the key. + +## Project Status & Versioning + +This library is considered production ready. Feedback and feature requests are appreciated. The API should be considered stable. There should be very few backwards-incompatible changes outside of major version updates (and only with good reason). + +This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `main`. Periodically, versions will be tagged from `main`. You can find all the releases on [the project releases page](https://github.com/golang-jwt/jwt/releases). + +**BREAKING CHANGES:*** +A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code. + +## Usage Tips + +### Signing vs Encryption + +A token is simply a JSON object that is signed by its author. this tells you exactly two things about the data: + +* The author of the token was in the possession of the signing secret +* The data has not been modified since it was signed + +It's important to know that JWT does not provide encryption, which means anyone who has access to the token can read its contents. If you need to protect (encrypt) the data, there is a companion spec, `JWE`, that provides this functionality. The companion project https://github.com/golang-jwt/jwe aims at a (very) experimental implementation of the JWE standard. + +### Choosing a Signing Method + +There are several signing methods available, and you should probably take the time to learn about the various options before choosing one. The principal design decision is most likely going to be symmetric vs asymmetric. + +Symmetric signing methods, such as HSA, use only a single secret. This is probably the simplest signing method to use since any `[]byte` can be used as a valid secret. They are also slightly computationally faster to use, though this rarely is enough to matter. Symmetric signing methods work the best when both producers and consumers of tokens are trusted, or even the same system. Since the same secret is used to both sign and validate tokens, you can't easily distribute the key for validation. + +Asymmetric signing methods, such as RSA, use different keys for signing and verifying tokens. This makes it possible to produce tokens with a private key, and allow any consumer to access the public key for verification. + +### Signing Methods and Key Types + +Each signing method expects a different object type for its signing keys. See the package documentation for details. Here are the most common ones: + +* The [HMAC signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation +* The [RSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation +* The [ECDSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation +* The [EdDSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#SigningMethodEd25519) (`Ed25519`) expect `ed25519.PrivateKey` for signing and `ed25519.PublicKey` for validation + +### JWT and OAuth + +It's worth mentioning that OAuth and JWT are not the same thing. A JWT token is simply a signed JSON object. It can be used anywhere such a thing is useful. There is some confusion, though, as JWT is the most common type of bearer token used in OAuth2 authentication. + +Without going too far down the rabbit hole, here's a description of the interaction of these technologies: + +* OAuth is a protocol for allowing an identity provider to be separate from the service a user is logging in to. For example, whenever you use Facebook to log into a different service (Yelp, Spotify, etc), you are using OAuth. +* OAuth defines several options for passing around authentication data. One popular method is called a "bearer token". A bearer token is simply a string that _should_ only be held by an authenticated user. Thus, simply presenting this token proves your identity. You can probably derive from here why a JWT might make a good bearer token. +* Because bearer tokens are used for authentication, it's important they're kept secret. This is why transactions that use bearer tokens typically happen over SSL. + +### Troubleshooting + +This library uses descriptive error messages whenever possible. If you are not getting the expected result, have a look at the errors. The most common place people get stuck is providing the correct type of key to the parser. See the above section on signing methods and key types. + +## More + +Documentation can be found [on pkg.go.dev](https://pkg.go.dev/github.com/golang-jwt/jwt/v4). + +The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. You'll also find several implementation examples in the documentation. + +[golang-jwt](https://github.com/orgs/golang-jwt) incorporates a modified version of the JWT logo, which is distributed under the terms of the [MIT License](https://github.com/jsonwebtoken/jsonwebtoken.github.io/blob/master/LICENSE.txt). diff --git a/vendor/github.com/golang-jwt/jwt/v4/SECURITY.md b/vendor/github.com/golang-jwt/jwt/v4/SECURITY.md new file mode 100644 index 00000000..b08402c3 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Supported Versions + +As of February 2022 (and until this document is updated), the latest version `v4` is supported. + +## Reporting a Vulnerability + +If you think you found a vulnerability, and even if you are not sure, please report it to jwt-go-security@googlegroups.com or one of the other [golang-jwt maintainers](https://github.com/orgs/golang-jwt/people). Please try be explicit, describe steps to reproduce the security issue with code example(s). + +You will receive a response within a timely manner. If the issue is confirmed, we will do our best to release a patch as soon as possible given the complexity of the problem. + +## Public Discussions + +Please avoid publicly discussing a potential security vulnerability. + +Let's take this offline and find a solution first, this limits the potential impact as much as possible. + +We appreciate your help! diff --git a/vendor/github.com/golang-jwt/jwt/v4/VERSION_HISTORY.md b/vendor/github.com/golang-jwt/jwt/v4/VERSION_HISTORY.md new file mode 100644 index 00000000..afbfc4e4 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/VERSION_HISTORY.md @@ -0,0 +1,135 @@ +## `jwt-go` Version History + +#### 4.0.0 + +* Introduces support for Go modules. The `v4` version will be backwards compatible with `v3.x.y`. + +#### 3.2.2 + +* Starting from this release, we are adopting the policy to support the most 2 recent versions of Go currently available. By the time of this release, this is Go 1.15 and 1.16 ([#28](https://github.com/golang-jwt/jwt/pull/28)). +* Fixed a potential issue that could occur when the verification of `exp`, `iat` or `nbf` was not required and contained invalid contents, i.e. non-numeric/date. Thanks for @thaJeztah for making us aware of that and @giorgos-f3 for originally reporting it to the formtech fork ([#40](https://github.com/golang-jwt/jwt/pull/40)). +* Added support for EdDSA / ED25519 ([#36](https://github.com/golang-jwt/jwt/pull/36)). +* Optimized allocations ([#33](https://github.com/golang-jwt/jwt/pull/33)). + +#### 3.2.1 + +* **Import Path Change**: See MIGRATION_GUIDE.md for tips on updating your code + * Changed the import path from `github.com/dgrijalva/jwt-go` to `github.com/golang-jwt/jwt` +* Fixed type confusing issue between `string` and `[]string` in `VerifyAudience` ([#12](https://github.com/golang-jwt/jwt/pull/12)). This fixes CVE-2020-26160 + +#### 3.2.0 + +* Added method `ParseUnverified` to allow users to split up the tasks of parsing and validation +* HMAC signing method returns `ErrInvalidKeyType` instead of `ErrInvalidKey` where appropriate +* Added options to `request.ParseFromRequest`, which allows for an arbitrary list of modifiers to parsing behavior. Initial set include `WithClaims` and `WithParser`. Existing usage of this function will continue to work as before. +* Deprecated `ParseFromRequestWithClaims` to simplify API in the future. + +#### 3.1.0 + +* Improvements to `jwt` command line tool +* Added `SkipClaimsValidation` option to `Parser` +* Documentation updates + +#### 3.0.0 + +* **Compatibility Breaking Changes**: See MIGRATION_GUIDE.md for tips on updating your code + * Dropped support for `[]byte` keys when using RSA signing methods. This convenience feature could contribute to security vulnerabilities involving mismatched key types with signing methods. + * `ParseFromRequest` has been moved to `request` subpackage and usage has changed + * The `Claims` property on `Token` is now type `Claims` instead of `map[string]interface{}`. The default value is type `MapClaims`, which is an alias to `map[string]interface{}`. This makes it possible to use a custom type when decoding claims. +* Other Additions and Changes + * Added `Claims` interface type to allow users to decode the claims into a custom type + * Added `ParseWithClaims`, which takes a third argument of type `Claims`. Use this function instead of `Parse` if you have a custom type you'd like to decode into. + * Dramatically improved the functionality and flexibility of `ParseFromRequest`, which is now in the `request` subpackage + * Added `ParseFromRequestWithClaims` which is the `FromRequest` equivalent of `ParseWithClaims` + * Added new interface type `Extractor`, which is used for extracting JWT strings from http requests. Used with `ParseFromRequest` and `ParseFromRequestWithClaims`. + * Added several new, more specific, validation errors to error type bitmask + * Moved examples from README to executable example files + * Signing method registry is now thread safe + * Added new property to `ValidationError`, which contains the raw error returned by calls made by parse/verify (such as those returned by keyfunc or json parser) + +#### 2.7.0 + +This will likely be the last backwards compatible release before 3.0.0, excluding essential bug fixes. + +* Added new option `-show` to the `jwt` command that will just output the decoded token without verifying +* Error text for expired tokens includes how long it's been expired +* Fixed incorrect error returned from `ParseRSAPublicKeyFromPEM` +* Documentation updates + +#### 2.6.0 + +* Exposed inner error within ValidationError +* Fixed validation errors when using UseJSONNumber flag +* Added several unit tests + +#### 2.5.0 + +* Added support for signing method none. You shouldn't use this. The API tries to make this clear. +* Updated/fixed some documentation +* Added more helpful error message when trying to parse tokens that begin with `BEARER ` + +#### 2.4.0 + +* Added new type, Parser, to allow for configuration of various parsing parameters + * You can now specify a list of valid signing methods. Anything outside this set will be rejected. + * You can now opt to use the `json.Number` type instead of `float64` when parsing token JSON +* Added support for [Travis CI](https://travis-ci.org/dgrijalva/jwt-go) +* Fixed some bugs with ECDSA parsing + +#### 2.3.0 + +* Added support for ECDSA signing methods +* Added support for RSA PSS signing methods (requires go v1.4) + +#### 2.2.0 + +* Gracefully handle a `nil` `Keyfunc` being passed to `Parse`. Result will now be the parsed token and an error, instead of a panic. + +#### 2.1.0 + +Backwards compatible API change that was missed in 2.0.0. + +* The `SignedString` method on `Token` now takes `interface{}` instead of `[]byte` + +#### 2.0.0 + +There were two major reasons for breaking backwards compatibility with this update. The first was a refactor required to expand the width of the RSA and HMAC-SHA signing implementations. There will likely be no required code changes to support this change. + +The second update, while unfortunately requiring a small change in integration, is required to open up this library to other signing methods. Not all keys used for all signing methods have a single standard on-disk representation. Requiring `[]byte` as the type for all keys proved too limiting. Additionally, this implementation allows for pre-parsed tokens to be reused, which might matter in an application that parses a high volume of tokens with a small set of keys. Backwards compatibilty has been maintained for passing `[]byte` to the RSA signing methods, but they will also accept `*rsa.PublicKey` and `*rsa.PrivateKey`. + +It is likely the only integration change required here will be to change `func(t *jwt.Token) ([]byte, error)` to `func(t *jwt.Token) (interface{}, error)` when calling `Parse`. + +* **Compatibility Breaking Changes** + * `SigningMethodHS256` is now `*SigningMethodHMAC` instead of `type struct` + * `SigningMethodRS256` is now `*SigningMethodRSA` instead of `type struct` + * `KeyFunc` now returns `interface{}` instead of `[]byte` + * `SigningMethod.Sign` now takes `interface{}` instead of `[]byte` for the key + * `SigningMethod.Verify` now takes `interface{}` instead of `[]byte` for the key +* Renamed type `SigningMethodHS256` to `SigningMethodHMAC`. Specific sizes are now just instances of this type. + * Added public package global `SigningMethodHS256` + * Added public package global `SigningMethodHS384` + * Added public package global `SigningMethodHS512` +* Renamed type `SigningMethodRS256` to `SigningMethodRSA`. Specific sizes are now just instances of this type. + * Added public package global `SigningMethodRS256` + * Added public package global `SigningMethodRS384` + * Added public package global `SigningMethodRS512` +* Moved sample private key for HMAC tests from an inline value to a file on disk. Value is unchanged. +* Refactored the RSA implementation to be easier to read +* Exposed helper methods `ParseRSAPrivateKeyFromPEM` and `ParseRSAPublicKeyFromPEM` + +#### 1.0.2 + +* Fixed bug in parsing public keys from certificates +* Added more tests around the parsing of keys for RS256 +* Code refactoring in RS256 implementation. No functional changes + +#### 1.0.1 + +* Fixed panic if RS256 signing method was passed an invalid key + +#### 1.0.0 + +* First versioned release +* API stabilized +* Supports creating, signing, parsing, and validating JWT tokens +* Supports RS256 and HS256 signing methods diff --git a/vendor/github.com/golang-jwt/jwt/v4/claims.go b/vendor/github.com/golang-jwt/jwt/v4/claims.go new file mode 100644 index 00000000..364cec87 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/claims.go @@ -0,0 +1,269 @@ +package jwt + +import ( + "crypto/subtle" + "fmt" + "time" +) + +// Claims must just have a Valid method that determines +// if the token is invalid for any supported reason +type Claims interface { + Valid() error +} + +// RegisteredClaims are a structured version of the JWT Claims Set, +// restricted to Registered Claim Names, as referenced at +// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 +// +// This type can be used on its own, but then additional private and +// public claims embedded in the JWT will not be parsed. The typical usecase +// therefore is to embedded this in a user-defined claim type. +// +// See examples for how to use this with your own claim types. +type RegisteredClaims struct { + // the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 + Issuer string `json:"iss,omitempty"` + + // the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2 + Subject string `json:"sub,omitempty"` + + // the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3 + Audience ClaimStrings `json:"aud,omitempty"` + + // the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4 + ExpiresAt *NumericDate `json:"exp,omitempty"` + + // the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5 + NotBefore *NumericDate `json:"nbf,omitempty"` + + // the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6 + IssuedAt *NumericDate `json:"iat,omitempty"` + + // the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7 + ID string `json:"jti,omitempty"` +} + +// Valid validates time based claims "exp, iat, nbf". +// There is no accounting for clock skew. +// As well, if any of the above claims are not in the token, it will still +// be considered a valid claim. +func (c RegisteredClaims) Valid() error { + vErr := new(ValidationError) + now := TimeFunc() + + // The claims below are optional, by default, so if they are set to the + // default value in Go, let's not fail the verification for them. + if !c.VerifyExpiresAt(now, false) { + delta := now.Sub(c.ExpiresAt.Time) + vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta) + vErr.Errors |= ValidationErrorExpired + } + + if !c.VerifyIssuedAt(now, false) { + vErr.Inner = ErrTokenUsedBeforeIssued + vErr.Errors |= ValidationErrorIssuedAt + } + + if !c.VerifyNotBefore(now, false) { + vErr.Inner = ErrTokenNotValidYet + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} + +// VerifyAudience compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *RegisteredClaims) VerifyAudience(cmp string, req bool) bool { + return verifyAud(c.Audience, cmp, req) +} + +// VerifyExpiresAt compares the exp claim against cmp (cmp < exp). +// If req is false, it will return true, if exp is unset. +func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool) bool { + if c.ExpiresAt == nil { + return verifyExp(nil, cmp, req) + } + + return verifyExp(&c.ExpiresAt.Time, cmp, req) +} + +// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat). +// If req is false, it will return true, if iat is unset. +func (c *RegisteredClaims) VerifyIssuedAt(cmp time.Time, req bool) bool { + if c.IssuedAt == nil { + return verifyIat(nil, cmp, req) + } + + return verifyIat(&c.IssuedAt.Time, cmp, req) +} + +// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf). +// If req is false, it will return true, if nbf is unset. +func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool) bool { + if c.NotBefore == nil { + return verifyNbf(nil, cmp, req) + } + + return verifyNbf(&c.NotBefore.Time, cmp, req) +} + +// VerifyIssuer compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *RegisteredClaims) VerifyIssuer(cmp string, req bool) bool { + return verifyIss(c.Issuer, cmp, req) +} + +// StandardClaims are a structured version of the JWT Claims Set, as referenced at +// https://datatracker.ietf.org/doc/html/rfc7519#section-4. They do not follow the +// specification exactly, since they were based on an earlier draft of the +// specification and not updated. The main difference is that they only +// support integer-based date fields and singular audiences. This might lead to +// incompatibilities with other JWT implementations. The use of this is discouraged, instead +// the newer RegisteredClaims struct should be used. +// +// Deprecated: Use RegisteredClaims instead for a forward-compatible way to access registered claims in a struct. +type StandardClaims struct { + Audience string `json:"aud,omitempty"` + ExpiresAt int64 `json:"exp,omitempty"` + Id string `json:"jti,omitempty"` + IssuedAt int64 `json:"iat,omitempty"` + Issuer string `json:"iss,omitempty"` + NotBefore int64 `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` +} + +// Valid validates time based claims "exp, iat, nbf". There is no accounting for clock skew. +// As well, if any of the above claims are not in the token, it will still +// be considered a valid claim. +func (c StandardClaims) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + // The claims below are optional, by default, so if they are set to the + // default value in Go, let's not fail the verification for them. + if !c.VerifyExpiresAt(now, false) { + delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0)) + vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta) + vErr.Errors |= ValidationErrorExpired + } + + if !c.VerifyIssuedAt(now, false) { + vErr.Inner = ErrTokenUsedBeforeIssued + vErr.Errors |= ValidationErrorIssuedAt + } + + if !c.VerifyNotBefore(now, false) { + vErr.Inner = ErrTokenNotValidYet + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} + +// VerifyAudience compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool { + return verifyAud([]string{c.Audience}, cmp, req) +} + +// VerifyExpiresAt compares the exp claim against cmp (cmp < exp). +// If req is false, it will return true, if exp is unset. +func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { + if c.ExpiresAt == 0 { + return verifyExp(nil, time.Unix(cmp, 0), req) + } + + t := time.Unix(c.ExpiresAt, 0) + return verifyExp(&t, time.Unix(cmp, 0), req) +} + +// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat). +// If req is false, it will return true, if iat is unset. +func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { + if c.IssuedAt == 0 { + return verifyIat(nil, time.Unix(cmp, 0), req) + } + + t := time.Unix(c.IssuedAt, 0) + return verifyIat(&t, time.Unix(cmp, 0), req) +} + +// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf). +// If req is false, it will return true, if nbf is unset. +func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { + if c.NotBefore == 0 { + return verifyNbf(nil, time.Unix(cmp, 0), req) + } + + t := time.Unix(c.NotBefore, 0) + return verifyNbf(&t, time.Unix(cmp, 0), req) +} + +// VerifyIssuer compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool { + return verifyIss(c.Issuer, cmp, req) +} + +// ----- helpers + +func verifyAud(aud []string, cmp string, required bool) bool { + if len(aud) == 0 { + return !required + } + // use a var here to keep constant time compare when looping over a number of claims + result := false + + var stringClaims string + for _, a := range aud { + if subtle.ConstantTimeCompare([]byte(a), []byte(cmp)) != 0 { + result = true + } + stringClaims = stringClaims + a + } + + // case where "" is sent in one or many aud claims + if len(stringClaims) == 0 { + return !required + } + + return result +} + +func verifyExp(exp *time.Time, now time.Time, required bool) bool { + if exp == nil { + return !required + } + return now.Before(*exp) +} + +func verifyIat(iat *time.Time, now time.Time, required bool) bool { + if iat == nil { + return !required + } + return now.After(*iat) || now.Equal(*iat) +} + +func verifyNbf(nbf *time.Time, now time.Time, required bool) bool { + if nbf == nil { + return !required + } + return now.After(*nbf) || now.Equal(*nbf) +} + +func verifyIss(iss string, cmp string, required bool) bool { + if iss == "" { + return !required + } + return subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/doc.go b/vendor/github.com/golang-jwt/jwt/v4/doc.go new file mode 100644 index 00000000..a86dc1a3 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/doc.go @@ -0,0 +1,4 @@ +// Package jwt is a Go implementation of JSON Web Tokens: http://self-issued.info/docs/draft-jones-json-web-token.html +// +// See README.md for more info. +package jwt diff --git a/vendor/github.com/golang-jwt/jwt/v4/ecdsa.go b/vendor/github.com/golang-jwt/jwt/v4/ecdsa.go new file mode 100644 index 00000000..eac023fc --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/ecdsa.go @@ -0,0 +1,142 @@ +package jwt + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "errors" + "math/big" +) + +var ( + // Sadly this is missing from crypto/ecdsa compared to crypto/rsa + ErrECDSAVerification = errors.New("crypto/ecdsa: verification error") +) + +// SigningMethodECDSA implements the ECDSA family of signing methods. +// Expects *ecdsa.PrivateKey for signing and *ecdsa.PublicKey for verification +type SigningMethodECDSA struct { + Name string + Hash crypto.Hash + KeySize int + CurveBits int +} + +// Specific instances for EC256 and company +var ( + SigningMethodES256 *SigningMethodECDSA + SigningMethodES384 *SigningMethodECDSA + SigningMethodES512 *SigningMethodECDSA +) + +func init() { + // ES256 + SigningMethodES256 = &SigningMethodECDSA{"ES256", crypto.SHA256, 32, 256} + RegisterSigningMethod(SigningMethodES256.Alg(), func() SigningMethod { + return SigningMethodES256 + }) + + // ES384 + SigningMethodES384 = &SigningMethodECDSA{"ES384", crypto.SHA384, 48, 384} + RegisterSigningMethod(SigningMethodES384.Alg(), func() SigningMethod { + return SigningMethodES384 + }) + + // ES512 + SigningMethodES512 = &SigningMethodECDSA{"ES512", crypto.SHA512, 66, 521} + RegisterSigningMethod(SigningMethodES512.Alg(), func() SigningMethod { + return SigningMethodES512 + }) +} + +func (m *SigningMethodECDSA) Alg() string { + return m.Name +} + +// Verify implements token verification for the SigningMethod. +// For this verify method, key must be an ecdsa.PublicKey struct +func (m *SigningMethodECDSA) Verify(signingString, signature string, key interface{}) error { + var err error + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + // Get the key + var ecdsaKey *ecdsa.PublicKey + switch k := key.(type) { + case *ecdsa.PublicKey: + ecdsaKey = k + default: + return ErrInvalidKeyType + } + + if len(sig) != 2*m.KeySize { + return ErrECDSAVerification + } + + r := big.NewInt(0).SetBytes(sig[:m.KeySize]) + s := big.NewInt(0).SetBytes(sig[m.KeySize:]) + + // Create hasher + if !m.Hash.Available() { + return ErrHashUnavailable + } + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Verify the signature + if verifystatus := ecdsa.Verify(ecdsaKey, hasher.Sum(nil), r, s); verifystatus { + return nil + } + + return ErrECDSAVerification +} + +// Sign implements token signing for the SigningMethod. +// For this signing method, key must be an ecdsa.PrivateKey struct +func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) (string, error) { + // Get the key + var ecdsaKey *ecdsa.PrivateKey + switch k := key.(type) { + case *ecdsa.PrivateKey: + ecdsaKey = k + default: + return "", ErrInvalidKeyType + } + + // Create the hasher + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Sign the string and return r, s + if r, s, err := ecdsa.Sign(rand.Reader, ecdsaKey, hasher.Sum(nil)); err == nil { + curveBits := ecdsaKey.Curve.Params().BitSize + + if m.CurveBits != curveBits { + return "", ErrInvalidKey + } + + keyBytes := curveBits / 8 + if curveBits%8 > 0 { + keyBytes += 1 + } + + // We serialize the outputs (r and s) into big-endian byte arrays + // padded with zeros on the left to make sure the sizes work out. + // Output must be 2*keyBytes long. + out := make([]byte, 2*keyBytes) + r.FillBytes(out[0:keyBytes]) // r is assigned to the first half of output. + s.FillBytes(out[keyBytes:]) // s is assigned to the second half of output. + + return EncodeSegment(out), nil + } else { + return "", err + } +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/ecdsa_utils.go b/vendor/github.com/golang-jwt/jwt/v4/ecdsa_utils.go new file mode 100644 index 00000000..5700636d --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/ecdsa_utils.go @@ -0,0 +1,69 @@ +package jwt + +import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "errors" +) + +var ( + ErrNotECPublicKey = errors.New("key is not a valid ECDSA public key") + ErrNotECPrivateKey = errors.New("key is not a valid ECDSA private key") +) + +// ParseECPrivateKeyFromPEM parses a PEM encoded Elliptic Curve Private Key Structure +func ParseECPrivateKeyFromPEM(key []byte) (*ecdsa.PrivateKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParseECPrivateKey(block.Bytes); err != nil { + if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil { + return nil, err + } + } + + var pkey *ecdsa.PrivateKey + var ok bool + if pkey, ok = parsedKey.(*ecdsa.PrivateKey); !ok { + return nil, ErrNotECPrivateKey + } + + return pkey, nil +} + +// ParseECPublicKeyFromPEM parses a PEM encoded PKCS1 or PKCS8 public key +func ParseECPublicKeyFromPEM(key []byte) (*ecdsa.PublicKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { + if cert, err := x509.ParseCertificate(block.Bytes); err == nil { + parsedKey = cert.PublicKey + } else { + return nil, err + } + } + + var pkey *ecdsa.PublicKey + var ok bool + if pkey, ok = parsedKey.(*ecdsa.PublicKey); !ok { + return nil, ErrNotECPublicKey + } + + return pkey, nil +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/ed25519.go b/vendor/github.com/golang-jwt/jwt/v4/ed25519.go new file mode 100644 index 00000000..07d3aacd --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/ed25519.go @@ -0,0 +1,85 @@ +package jwt + +import ( + "errors" + + "crypto" + "crypto/ed25519" + "crypto/rand" +) + +var ( + ErrEd25519Verification = errors.New("ed25519: verification error") +) + +// SigningMethodEd25519 implements the EdDSA family. +// Expects ed25519.PrivateKey for signing and ed25519.PublicKey for verification +type SigningMethodEd25519 struct{} + +// Specific instance for EdDSA +var ( + SigningMethodEdDSA *SigningMethodEd25519 +) + +func init() { + SigningMethodEdDSA = &SigningMethodEd25519{} + RegisterSigningMethod(SigningMethodEdDSA.Alg(), func() SigningMethod { + return SigningMethodEdDSA + }) +} + +func (m *SigningMethodEd25519) Alg() string { + return "EdDSA" +} + +// Verify implements token verification for the SigningMethod. +// For this verify method, key must be an ed25519.PublicKey +func (m *SigningMethodEd25519) Verify(signingString, signature string, key interface{}) error { + var err error + var ed25519Key ed25519.PublicKey + var ok bool + + if ed25519Key, ok = key.(ed25519.PublicKey); !ok { + return ErrInvalidKeyType + } + + if len(ed25519Key) != ed25519.PublicKeySize { + return ErrInvalidKey + } + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + // Verify the signature + if !ed25519.Verify(ed25519Key, []byte(signingString), sig) { + return ErrEd25519Verification + } + + return nil +} + +// Sign implements token signing for the SigningMethod. +// For this signing method, key must be an ed25519.PrivateKey +func (m *SigningMethodEd25519) Sign(signingString string, key interface{}) (string, error) { + var ed25519Key crypto.Signer + var ok bool + + if ed25519Key, ok = key.(crypto.Signer); !ok { + return "", ErrInvalidKeyType + } + + if _, ok := ed25519Key.Public().(ed25519.PublicKey); !ok { + return "", ErrInvalidKey + } + + // Sign the string and return the encoded result + // ed25519 performs a two-pass hash as part of its algorithm. Therefore, we need to pass a non-prehashed message into the Sign function, as indicated by crypto.Hash(0) + sig, err := ed25519Key.Sign(rand.Reader, []byte(signingString), crypto.Hash(0)) + if err != nil { + return "", err + } + return EncodeSegment(sig), nil +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/ed25519_utils.go b/vendor/github.com/golang-jwt/jwt/v4/ed25519_utils.go new file mode 100644 index 00000000..cdb5e68e --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/ed25519_utils.go @@ -0,0 +1,64 @@ +package jwt + +import ( + "crypto" + "crypto/ed25519" + "crypto/x509" + "encoding/pem" + "errors" +) + +var ( + ErrNotEdPrivateKey = errors.New("key is not a valid Ed25519 private key") + ErrNotEdPublicKey = errors.New("key is not a valid Ed25519 public key") +) + +// ParseEdPrivateKeyFromPEM parses a PEM-encoded Edwards curve private key +func ParseEdPrivateKeyFromPEM(key []byte) (crypto.PrivateKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil { + return nil, err + } + + var pkey ed25519.PrivateKey + var ok bool + if pkey, ok = parsedKey.(ed25519.PrivateKey); !ok { + return nil, ErrNotEdPrivateKey + } + + return pkey, nil +} + +// ParseEdPublicKeyFromPEM parses a PEM-encoded Edwards curve public key +func ParseEdPublicKeyFromPEM(key []byte) (crypto.PublicKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { + return nil, err + } + + var pkey ed25519.PublicKey + var ok bool + if pkey, ok = parsedKey.(ed25519.PublicKey); !ok { + return nil, ErrNotEdPublicKey + } + + return pkey, nil +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/errors.go b/vendor/github.com/golang-jwt/jwt/v4/errors.go new file mode 100644 index 00000000..10ac8835 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/errors.go @@ -0,0 +1,112 @@ +package jwt + +import ( + "errors" +) + +// Error constants +var ( + ErrInvalidKey = errors.New("key is invalid") + ErrInvalidKeyType = errors.New("key is of invalid type") + ErrHashUnavailable = errors.New("the requested hash function is unavailable") + + ErrTokenMalformed = errors.New("token is malformed") + ErrTokenUnverifiable = errors.New("token is unverifiable") + ErrTokenSignatureInvalid = errors.New("token signature is invalid") + + ErrTokenInvalidAudience = errors.New("token has invalid audience") + ErrTokenExpired = errors.New("token is expired") + ErrTokenUsedBeforeIssued = errors.New("token used before issued") + ErrTokenInvalidIssuer = errors.New("token has invalid issuer") + ErrTokenNotValidYet = errors.New("token is not valid yet") + ErrTokenInvalidId = errors.New("token has invalid id") + ErrTokenInvalidClaims = errors.New("token has invalid claims") +) + +// The errors that might occur when parsing and validating a token +const ( + ValidationErrorMalformed uint32 = 1 << iota // Token is malformed + ValidationErrorUnverifiable // Token could not be verified because of signing problems + ValidationErrorSignatureInvalid // Signature validation failed + + // Standard Claim validation errors + ValidationErrorAudience // AUD validation failed + ValidationErrorExpired // EXP validation failed + ValidationErrorIssuedAt // IAT validation failed + ValidationErrorIssuer // ISS validation failed + ValidationErrorNotValidYet // NBF validation failed + ValidationErrorId // JTI validation failed + ValidationErrorClaimsInvalid // Generic claims validation error +) + +// NewValidationError is a helper for constructing a ValidationError with a string error message +func NewValidationError(errorText string, errorFlags uint32) *ValidationError { + return &ValidationError{ + text: errorText, + Errors: errorFlags, + } +} + +// ValidationError represents an error from Parse if token is not valid +type ValidationError struct { + Inner error // stores the error returned by external dependencies, i.e.: KeyFunc + Errors uint32 // bitfield. see ValidationError... constants + text string // errors that do not have a valid error just have text +} + +// Error is the implementation of the err interface. +func (e ValidationError) Error() string { + if e.Inner != nil { + return e.Inner.Error() + } else if e.text != "" { + return e.text + } else { + return "token is invalid" + } +} + +// Unwrap gives errors.Is and errors.As access to the inner error. +func (e *ValidationError) Unwrap() error { + return e.Inner +} + +// No errors +func (e *ValidationError) valid() bool { + return e.Errors == 0 +} + +// Is checks if this ValidationError is of the supplied error. We are first checking for the exact error message +// by comparing the inner error message. If that fails, we compare using the error flags. This way we can use +// custom error messages (mainly for backwards compatability) and still leverage errors.Is using the global error variables. +func (e *ValidationError) Is(err error) bool { + // Check, if our inner error is a direct match + if errors.Is(errors.Unwrap(e), err) { + return true + } + + // Otherwise, we need to match using our error flags + switch err { + case ErrTokenMalformed: + return e.Errors&ValidationErrorMalformed != 0 + case ErrTokenUnverifiable: + return e.Errors&ValidationErrorUnverifiable != 0 + case ErrTokenSignatureInvalid: + return e.Errors&ValidationErrorSignatureInvalid != 0 + case ErrTokenInvalidAudience: + return e.Errors&ValidationErrorAudience != 0 + case ErrTokenExpired: + return e.Errors&ValidationErrorExpired != 0 + case ErrTokenUsedBeforeIssued: + return e.Errors&ValidationErrorIssuedAt != 0 + case ErrTokenInvalidIssuer: + return e.Errors&ValidationErrorIssuer != 0 + case ErrTokenNotValidYet: + return e.Errors&ValidationErrorNotValidYet != 0 + case ErrTokenInvalidId: + return e.Errors&ValidationErrorId != 0 + case ErrTokenInvalidClaims: + return e.Errors&ValidationErrorClaimsInvalid != 0 + } + + return false +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/hmac.go b/vendor/github.com/golang-jwt/jwt/v4/hmac.go new file mode 100644 index 00000000..011f68a2 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/hmac.go @@ -0,0 +1,95 @@ +package jwt + +import ( + "crypto" + "crypto/hmac" + "errors" +) + +// SigningMethodHMAC implements the HMAC-SHA family of signing methods. +// Expects key type of []byte for both signing and validation +type SigningMethodHMAC struct { + Name string + Hash crypto.Hash +} + +// Specific instances for HS256 and company +var ( + SigningMethodHS256 *SigningMethodHMAC + SigningMethodHS384 *SigningMethodHMAC + SigningMethodHS512 *SigningMethodHMAC + ErrSignatureInvalid = errors.New("signature is invalid") +) + +func init() { + // HS256 + SigningMethodHS256 = &SigningMethodHMAC{"HS256", crypto.SHA256} + RegisterSigningMethod(SigningMethodHS256.Alg(), func() SigningMethod { + return SigningMethodHS256 + }) + + // HS384 + SigningMethodHS384 = &SigningMethodHMAC{"HS384", crypto.SHA384} + RegisterSigningMethod(SigningMethodHS384.Alg(), func() SigningMethod { + return SigningMethodHS384 + }) + + // HS512 + SigningMethodHS512 = &SigningMethodHMAC{"HS512", crypto.SHA512} + RegisterSigningMethod(SigningMethodHS512.Alg(), func() SigningMethod { + return SigningMethodHS512 + }) +} + +func (m *SigningMethodHMAC) Alg() string { + return m.Name +} + +// Verify implements token verification for the SigningMethod. Returns nil if the signature is valid. +func (m *SigningMethodHMAC) Verify(signingString, signature string, key interface{}) error { + // Verify the key is the right type + keyBytes, ok := key.([]byte) + if !ok { + return ErrInvalidKeyType + } + + // Decode signature, for comparison + sig, err := DecodeSegment(signature) + if err != nil { + return err + } + + // Can we use the specified hashing method? + if !m.Hash.Available() { + return ErrHashUnavailable + } + + // This signing method is symmetric, so we validate the signature + // by reproducing the signature from the signing string and key, then + // comparing that against the provided signature. + hasher := hmac.New(m.Hash.New, keyBytes) + hasher.Write([]byte(signingString)) + if !hmac.Equal(sig, hasher.Sum(nil)) { + return ErrSignatureInvalid + } + + // No validation errors. Signature is good. + return nil +} + +// Sign implements token signing for the SigningMethod. +// Key must be []byte +func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) { + if keyBytes, ok := key.([]byte); ok { + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := hmac.New(m.Hash.New, keyBytes) + hasher.Write([]byte(signingString)) + + return EncodeSegment(hasher.Sum(nil)), nil + } + + return "", ErrInvalidKeyType +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/map_claims.go b/vendor/github.com/golang-jwt/jwt/v4/map_claims.go new file mode 100644 index 00000000..2700d64a --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/map_claims.go @@ -0,0 +1,151 @@ +package jwt + +import ( + "encoding/json" + "errors" + "time" + // "fmt" +) + +// MapClaims is a claims type that uses the map[string]interface{} for JSON decoding. +// This is the default claims type if you don't supply one +type MapClaims map[string]interface{} + +// VerifyAudience Compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyAudience(cmp string, req bool) bool { + var aud []string + switch v := m["aud"].(type) { + case string: + aud = append(aud, v) + case []string: + aud = v + case []interface{}: + for _, a := range v { + vs, ok := a.(string) + if !ok { + return false + } + aud = append(aud, vs) + } + } + return verifyAud(aud, cmp, req) +} + +// VerifyExpiresAt compares the exp claim against cmp (cmp <= exp). +// If req is false, it will return true, if exp is unset. +func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { + cmpTime := time.Unix(cmp, 0) + + v, ok := m["exp"] + if !ok { + return !req + } + + switch exp := v.(type) { + case float64: + if exp == 0 { + return verifyExp(nil, cmpTime, req) + } + + return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req) + case json.Number: + v, _ := exp.Float64() + + return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req) + } + + return false +} + +// VerifyIssuedAt compares the exp claim against cmp (cmp >= iat). +// If req is false, it will return true, if iat is unset. +func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { + cmpTime := time.Unix(cmp, 0) + + v, ok := m["iat"] + if !ok { + return !req + } + + switch iat := v.(type) { + case float64: + if iat == 0 { + return verifyIat(nil, cmpTime, req) + } + + return verifyIat(&newNumericDateFromSeconds(iat).Time, cmpTime, req) + case json.Number: + v, _ := iat.Float64() + + return verifyIat(&newNumericDateFromSeconds(v).Time, cmpTime, req) + } + + return false +} + +// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf). +// If req is false, it will return true, if nbf is unset. +func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool { + cmpTime := time.Unix(cmp, 0) + + v, ok := m["nbf"] + if !ok { + return !req + } + + switch nbf := v.(type) { + case float64: + if nbf == 0 { + return verifyNbf(nil, cmpTime, req) + } + + return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req) + case json.Number: + v, _ := nbf.Float64() + + return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req) + } + + return false +} + +// VerifyIssuer compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyIssuer(cmp string, req bool) bool { + iss, _ := m["iss"].(string) + return verifyIss(iss, cmp, req) +} + +// Valid validates time based claims "exp, iat, nbf". +// There is no accounting for clock skew. +// As well, if any of the above claims are not in the token, it will still +// be considered a valid claim. +func (m MapClaims) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + if !m.VerifyExpiresAt(now, false) { + // TODO(oxisto): this should be replaced with ErrTokenExpired + vErr.Inner = errors.New("Token is expired") + vErr.Errors |= ValidationErrorExpired + } + + if !m.VerifyIssuedAt(now, false) { + // TODO(oxisto): this should be replaced with ErrTokenUsedBeforeIssued + vErr.Inner = errors.New("Token used before issued") + vErr.Errors |= ValidationErrorIssuedAt + } + + if !m.VerifyNotBefore(now, false) { + // TODO(oxisto): this should be replaced with ErrTokenNotValidYet + vErr.Inner = errors.New("Token is not valid yet") + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/none.go b/vendor/github.com/golang-jwt/jwt/v4/none.go new file mode 100644 index 00000000..f19835d2 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/none.go @@ -0,0 +1,52 @@ +package jwt + +// SigningMethodNone implements the none signing method. This is required by the spec +// but you probably should never use it. +var SigningMethodNone *signingMethodNone + +const UnsafeAllowNoneSignatureType unsafeNoneMagicConstant = "none signing method allowed" + +var NoneSignatureTypeDisallowedError error + +type signingMethodNone struct{} +type unsafeNoneMagicConstant string + +func init() { + SigningMethodNone = &signingMethodNone{} + NoneSignatureTypeDisallowedError = NewValidationError("'none' signature type is not allowed", ValidationErrorSignatureInvalid) + + RegisterSigningMethod(SigningMethodNone.Alg(), func() SigningMethod { + return SigningMethodNone + }) +} + +func (m *signingMethodNone) Alg() string { + return "none" +} + +// Only allow 'none' alg type if UnsafeAllowNoneSignatureType is specified as the key +func (m *signingMethodNone) Verify(signingString, signature string, key interface{}) (err error) { + // Key must be UnsafeAllowNoneSignatureType to prevent accidentally + // accepting 'none' signing method + if _, ok := key.(unsafeNoneMagicConstant); !ok { + return NoneSignatureTypeDisallowedError + } + // If signing method is none, signature must be an empty string + if signature != "" { + return NewValidationError( + "'none' signing method with non-empty signature", + ValidationErrorSignatureInvalid, + ) + } + + // Accept 'none' signing method. + return nil +} + +// Only allow 'none' signing if UnsafeAllowNoneSignatureType is specified as the key +func (m *signingMethodNone) Sign(signingString string, key interface{}) (string, error) { + if _, ok := key.(unsafeNoneMagicConstant); ok { + return "", nil + } + return "", NoneSignatureTypeDisallowedError +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/parser.go b/vendor/github.com/golang-jwt/jwt/v4/parser.go new file mode 100644 index 00000000..c0a6f692 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/parser.go @@ -0,0 +1,177 @@ +package jwt + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +type Parser struct { + // If populated, only these methods will be considered valid. + // + // Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead. + ValidMethods []string + + // Use JSON Number format in JSON decoder. + // + // Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead. + UseJSONNumber bool + + // Skip claims validation during token parsing. + // + // Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead. + SkipClaimsValidation bool +} + +// NewParser creates a new Parser with the specified options +func NewParser(options ...ParserOption) *Parser { + p := &Parser{} + + // loop through our parsing options and apply them + for _, option := range options { + option(p) + } + + return p +} + +// Parse parses, validates, verifies the signature and returns the parsed token. +// keyFunc will receive the parsed token and should return the key for validating. +func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { + return p.ParseWithClaims(tokenString, MapClaims{}, keyFunc) +} + +// ParseWithClaims parses, validates, and verifies like Parse, but supplies a default object implementing the Claims +// interface. This provides default values which can be overridden and allows a caller to use their own type, rather +// than the default MapClaims implementation of Claims. +// +// Note: If you provide a custom claim implementation that embeds one of the standard claims (such as RegisteredClaims), +// make sure that a) you either embed a non-pointer version of the claims or b) if you are using a pointer, allocate the +// proper memory for it before passing in the overall claims, otherwise you might run into a panic. +func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { + token, parts, err := p.ParseUnverified(tokenString, claims) + if err != nil { + return token, err + } + + // Verify signing method is in the required set + if p.ValidMethods != nil { + var signingMethodValid = false + var alg = token.Method.Alg() + for _, m := range p.ValidMethods { + if m == alg { + signingMethodValid = true + break + } + } + if !signingMethodValid { + // signing method is not in the listed set + return token, NewValidationError(fmt.Sprintf("signing method %v is invalid", alg), ValidationErrorSignatureInvalid) + } + } + + // Lookup key + var key interface{} + if keyFunc == nil { + // keyFunc was not provided. short circuiting validation + return token, NewValidationError("no Keyfunc was provided.", ValidationErrorUnverifiable) + } + if key, err = keyFunc(token); err != nil { + // keyFunc returned an error + if ve, ok := err.(*ValidationError); ok { + return token, ve + } + return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable} + } + + vErr := &ValidationError{} + + // Validate Claims + if !p.SkipClaimsValidation { + if err := token.Claims.Valid(); err != nil { + + // If the Claims Valid returned an error, check if it is a validation error, + // If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set + if e, ok := err.(*ValidationError); !ok { + vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid} + } else { + vErr = e + } + } + } + + // Perform validation + token.Signature = parts[2] + if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil { + vErr.Inner = err + vErr.Errors |= ValidationErrorSignatureInvalid + } + + if vErr.valid() { + token.Valid = true + return token, nil + } + + return token, vErr +} + +// ParseUnverified parses the token but doesn't validate the signature. +// +// WARNING: Don't use this method unless you know what you're doing. +// +// It's only ever useful in cases where you know the signature is valid (because it has +// been checked previously in the stack) and you want to extract values from it. +func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) { + parts = strings.Split(tokenString, ".") + if len(parts) != 3 { + return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed) + } + + token = &Token{Raw: tokenString} + + // parse Header + var headerBytes []byte + if headerBytes, err = DecodeSegment(parts[0]); err != nil { + if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") { + return token, parts, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed) + } + return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} + } + if err = json.Unmarshal(headerBytes, &token.Header); err != nil { + return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} + } + + // parse Claims + var claimBytes []byte + token.Claims = claims + + if claimBytes, err = DecodeSegment(parts[1]); err != nil { + return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} + } + dec := json.NewDecoder(bytes.NewBuffer(claimBytes)) + if p.UseJSONNumber { + dec.UseNumber() + } + // JSON Decode. Special case for map type to avoid weird pointer behavior + if c, ok := token.Claims.(MapClaims); ok { + err = dec.Decode(&c) + } else { + err = dec.Decode(&claims) + } + // Handle decode error + if err != nil { + return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} + } + + // Lookup signature method + if method, ok := token.Header["alg"].(string); ok { + if token.Method = GetSigningMethod(method); token.Method == nil { + return token, parts, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable) + } + } else { + return token, parts, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable) + } + + return token, parts, nil +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/parser_option.go b/vendor/github.com/golang-jwt/jwt/v4/parser_option.go new file mode 100644 index 00000000..6ea6f952 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/parser_option.go @@ -0,0 +1,29 @@ +package jwt + +// ParserOption is used to implement functional-style options that modify the behavior of the parser. To add +// new options, just create a function (ideally beginning with With or Without) that returns an anonymous function that +// takes a *Parser type as input and manipulates its configuration accordingly. +type ParserOption func(*Parser) + +// WithValidMethods is an option to supply algorithm methods that the parser will check. Only those methods will be considered valid. +// It is heavily encouraged to use this option in order to prevent attacks such as https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/. +func WithValidMethods(methods []string) ParserOption { + return func(p *Parser) { + p.ValidMethods = methods + } +} + +// WithJSONNumber is an option to configure the underlying JSON parser with UseNumber +func WithJSONNumber() ParserOption { + return func(p *Parser) { + p.UseJSONNumber = true + } +} + +// WithoutClaimsValidation is an option to disable claims validation. This option should only be used if you exactly know +// what you are doing. +func WithoutClaimsValidation() ParserOption { + return func(p *Parser) { + p.SkipClaimsValidation = true + } +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/rsa.go b/vendor/github.com/golang-jwt/jwt/v4/rsa.go new file mode 100644 index 00000000..b910b19c --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/rsa.go @@ -0,0 +1,101 @@ +package jwt + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" +) + +// SigningMethodRSA implements the RSA family of signing methods. +// Expects *rsa.PrivateKey for signing and *rsa.PublicKey for validation +type SigningMethodRSA struct { + Name string + Hash crypto.Hash +} + +// Specific instances for RS256 and company +var ( + SigningMethodRS256 *SigningMethodRSA + SigningMethodRS384 *SigningMethodRSA + SigningMethodRS512 *SigningMethodRSA +) + +func init() { + // RS256 + SigningMethodRS256 = &SigningMethodRSA{"RS256", crypto.SHA256} + RegisterSigningMethod(SigningMethodRS256.Alg(), func() SigningMethod { + return SigningMethodRS256 + }) + + // RS384 + SigningMethodRS384 = &SigningMethodRSA{"RS384", crypto.SHA384} + RegisterSigningMethod(SigningMethodRS384.Alg(), func() SigningMethod { + return SigningMethodRS384 + }) + + // RS512 + SigningMethodRS512 = &SigningMethodRSA{"RS512", crypto.SHA512} + RegisterSigningMethod(SigningMethodRS512.Alg(), func() SigningMethod { + return SigningMethodRS512 + }) +} + +func (m *SigningMethodRSA) Alg() string { + return m.Name +} + +// Verify implements token verification for the SigningMethod +// For this signing method, must be an *rsa.PublicKey structure. +func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error { + var err error + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + var rsaKey *rsa.PublicKey + var ok bool + + if rsaKey, ok = key.(*rsa.PublicKey); !ok { + return ErrInvalidKeyType + } + + // Create hasher + if !m.Hash.Available() { + return ErrHashUnavailable + } + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Verify the signature + return rsa.VerifyPKCS1v15(rsaKey, m.Hash, hasher.Sum(nil), sig) +} + +// Sign implements token signing for the SigningMethod +// For this signing method, must be an *rsa.PrivateKey structure. +func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) { + var rsaKey *rsa.PrivateKey + var ok bool + + // Validate type of key + if rsaKey, ok = key.(*rsa.PrivateKey); !ok { + return "", ErrInvalidKey + } + + // Create the hasher + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Sign the string and return the encoded bytes + if sigBytes, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, m.Hash, hasher.Sum(nil)); err == nil { + return EncodeSegment(sigBytes), nil + } else { + return "", err + } +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/rsa_pss.go b/vendor/github.com/golang-jwt/jwt/v4/rsa_pss.go new file mode 100644 index 00000000..4fd6f9e6 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/rsa_pss.go @@ -0,0 +1,143 @@ +//go:build go1.4 +// +build go1.4 + +package jwt + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" +) + +// SigningMethodRSAPSS implements the RSAPSS family of signing methods signing methods +type SigningMethodRSAPSS struct { + *SigningMethodRSA + Options *rsa.PSSOptions + // VerifyOptions is optional. If set overrides Options for rsa.VerifyPPS. + // Used to accept tokens signed with rsa.PSSSaltLengthAuto, what doesn't follow + // https://tools.ietf.org/html/rfc7518#section-3.5 but was used previously. + // See https://github.com/dgrijalva/jwt-go/issues/285#issuecomment-437451244 for details. + VerifyOptions *rsa.PSSOptions +} + +// Specific instances for RS/PS and company. +var ( + SigningMethodPS256 *SigningMethodRSAPSS + SigningMethodPS384 *SigningMethodRSAPSS + SigningMethodPS512 *SigningMethodRSAPSS +) + +func init() { + // PS256 + SigningMethodPS256 = &SigningMethodRSAPSS{ + SigningMethodRSA: &SigningMethodRSA{ + Name: "PS256", + Hash: crypto.SHA256, + }, + Options: &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthEqualsHash, + }, + VerifyOptions: &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + }, + } + RegisterSigningMethod(SigningMethodPS256.Alg(), func() SigningMethod { + return SigningMethodPS256 + }) + + // PS384 + SigningMethodPS384 = &SigningMethodRSAPSS{ + SigningMethodRSA: &SigningMethodRSA{ + Name: "PS384", + Hash: crypto.SHA384, + }, + Options: &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthEqualsHash, + }, + VerifyOptions: &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + }, + } + RegisterSigningMethod(SigningMethodPS384.Alg(), func() SigningMethod { + return SigningMethodPS384 + }) + + // PS512 + SigningMethodPS512 = &SigningMethodRSAPSS{ + SigningMethodRSA: &SigningMethodRSA{ + Name: "PS512", + Hash: crypto.SHA512, + }, + Options: &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthEqualsHash, + }, + VerifyOptions: &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + }, + } + RegisterSigningMethod(SigningMethodPS512.Alg(), func() SigningMethod { + return SigningMethodPS512 + }) +} + +// Verify implements token verification for the SigningMethod. +// For this verify method, key must be an rsa.PublicKey struct +func (m *SigningMethodRSAPSS) Verify(signingString, signature string, key interface{}) error { + var err error + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + var rsaKey *rsa.PublicKey + switch k := key.(type) { + case *rsa.PublicKey: + rsaKey = k + default: + return ErrInvalidKey + } + + // Create hasher + if !m.Hash.Available() { + return ErrHashUnavailable + } + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + opts := m.Options + if m.VerifyOptions != nil { + opts = m.VerifyOptions + } + + return rsa.VerifyPSS(rsaKey, m.Hash, hasher.Sum(nil), sig, opts) +} + +// Sign implements token signing for the SigningMethod. +// For this signing method, key must be an rsa.PrivateKey struct +func (m *SigningMethodRSAPSS) Sign(signingString string, key interface{}) (string, error) { + var rsaKey *rsa.PrivateKey + + switch k := key.(type) { + case *rsa.PrivateKey: + rsaKey = k + default: + return "", ErrInvalidKeyType + } + + // Create the hasher + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Sign the string and return the encoded bytes + if sigBytes, err := rsa.SignPSS(rand.Reader, rsaKey, m.Hash, hasher.Sum(nil), m.Options); err == nil { + return EncodeSegment(sigBytes), nil + } else { + return "", err + } +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/rsa_utils.go b/vendor/github.com/golang-jwt/jwt/v4/rsa_utils.go new file mode 100644 index 00000000..1966c450 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/rsa_utils.go @@ -0,0 +1,105 @@ +package jwt + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" +) + +var ( + ErrKeyMustBePEMEncoded = errors.New("invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key") + ErrNotRSAPrivateKey = errors.New("key is not a valid RSA private key") + ErrNotRSAPublicKey = errors.New("key is not a valid RSA public key") +) + +// ParseRSAPrivateKeyFromPEM parses a PEM encoded PKCS1 or PKCS8 private key +func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + var parsedKey interface{} + if parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil { + if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil { + return nil, err + } + } + + var pkey *rsa.PrivateKey + var ok bool + if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok { + return nil, ErrNotRSAPrivateKey + } + + return pkey, nil +} + +// ParseRSAPrivateKeyFromPEMWithPassword parses a PEM encoded PKCS1 or PKCS8 private key protected with password +// +// Deprecated: This function is deprecated and should not be used anymore. It uses the deprecated x509.DecryptPEMBlock +// function, which was deprecated since RFC 1423 is regarded insecure by design. Unfortunately, there is no alternative +// in the Go standard library for now. See https://github.com/golang/go/issues/8860. +func ParseRSAPrivateKeyFromPEMWithPassword(key []byte, password string) (*rsa.PrivateKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + var parsedKey interface{} + + var blockDecrypted []byte + if blockDecrypted, err = x509.DecryptPEMBlock(block, []byte(password)); err != nil { + return nil, err + } + + if parsedKey, err = x509.ParsePKCS1PrivateKey(blockDecrypted); err != nil { + if parsedKey, err = x509.ParsePKCS8PrivateKey(blockDecrypted); err != nil { + return nil, err + } + } + + var pkey *rsa.PrivateKey + var ok bool + if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok { + return nil, ErrNotRSAPrivateKey + } + + return pkey, nil +} + +// ParseRSAPublicKeyFromPEM parses a PEM encoded PKCS1 or PKCS8 public key +func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { + if cert, err := x509.ParseCertificate(block.Bytes); err == nil { + parsedKey = cert.PublicKey + } else { + return nil, err + } + } + + var pkey *rsa.PublicKey + var ok bool + if pkey, ok = parsedKey.(*rsa.PublicKey); !ok { + return nil, ErrNotRSAPublicKey + } + + return pkey, nil +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/signing_method.go b/vendor/github.com/golang-jwt/jwt/v4/signing_method.go new file mode 100644 index 00000000..241ae9c6 --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/signing_method.go @@ -0,0 +1,46 @@ +package jwt + +import ( + "sync" +) + +var signingMethods = map[string]func() SigningMethod{} +var signingMethodLock = new(sync.RWMutex) + +// SigningMethod can be used add new methods for signing or verifying tokens. +type SigningMethod interface { + Verify(signingString, signature string, key interface{}) error // Returns nil if signature is valid + Sign(signingString string, key interface{}) (string, error) // Returns encoded signature or error + Alg() string // returns the alg identifier for this method (example: 'HS256') +} + +// RegisterSigningMethod registers the "alg" name and a factory function for signing method. +// This is typically done during init() in the method's implementation +func RegisterSigningMethod(alg string, f func() SigningMethod) { + signingMethodLock.Lock() + defer signingMethodLock.Unlock() + + signingMethods[alg] = f +} + +// GetSigningMethod retrieves a signing method from an "alg" string +func GetSigningMethod(alg string) (method SigningMethod) { + signingMethodLock.RLock() + defer signingMethodLock.RUnlock() + + if methodF, ok := signingMethods[alg]; ok { + method = methodF() + } + return +} + +// GetAlgorithms returns a list of registered "alg" names +func GetAlgorithms() (algs []string) { + signingMethodLock.RLock() + defer signingMethodLock.RUnlock() + + for alg := range signingMethods { + algs = append(algs, alg) + } + return +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/token.go b/vendor/github.com/golang-jwt/jwt/v4/token.go new file mode 100644 index 00000000..786b275c --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/token.go @@ -0,0 +1,143 @@ +package jwt + +import ( + "encoding/base64" + "encoding/json" + "strings" + "time" +) + +// DecodePaddingAllowed will switch the codec used for decoding JWTs respectively. Note that the JWS RFC7515 +// states that the tokens will utilize a Base64url encoding with no padding. Unfortunately, some implementations +// of JWT are producing non-standard tokens, and thus require support for decoding. Note that this is a global +// variable, and updating it will change the behavior on a package level, and is also NOT go-routine safe. +// To use the non-recommended decoding, set this boolean to `true` prior to using this package. +var DecodePaddingAllowed bool + +// DecodeStrict will switch the codec used for decoding JWTs into strict mode. +// In this mode, the decoder requires that trailing padding bits are zero, as described in RFC 4648 section 3.5. +// Note that this is a global variable, and updating it will change the behavior on a package level, and is also NOT go-routine safe. +// To use strict decoding, set this boolean to `true` prior to using this package. +var DecodeStrict bool + +// TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time). +// You can override it to use another time value. This is useful for testing or if your +// server uses a different time zone than your tokens. +var TimeFunc = time.Now + +// Keyfunc will be used by the Parse methods as a callback function to supply +// the key for verification. The function receives the parsed, +// but unverified Token. This allows you to use properties in the +// Header of the token (such as `kid`) to identify which key to use. +type Keyfunc func(*Token) (interface{}, error) + +// Token represents a JWT Token. Different fields will be used depending on whether you're +// creating or parsing/verifying a token. +type Token struct { + Raw string // The raw token. Populated when you Parse a token + Method SigningMethod // The signing method used or to be used + Header map[string]interface{} // The first segment of the token + Claims Claims // The second segment of the token + Signature string // The third segment of the token. Populated when you Parse a token + Valid bool // Is the token valid? Populated when you Parse/Verify a token +} + +// New creates a new Token with the specified signing method and an empty map of claims. +func New(method SigningMethod) *Token { + return NewWithClaims(method, MapClaims{}) +} + +// NewWithClaims creates a new Token with the specified signing method and claims. +func NewWithClaims(method SigningMethod, claims Claims) *Token { + return &Token{ + Header: map[string]interface{}{ + "typ": "JWT", + "alg": method.Alg(), + }, + Claims: claims, + Method: method, + } +} + +// SignedString creates and returns a complete, signed JWT. +// The token is signed using the SigningMethod specified in the token. +func (t *Token) SignedString(key interface{}) (string, error) { + var sig, sstr string + var err error + if sstr, err = t.SigningString(); err != nil { + return "", err + } + if sig, err = t.Method.Sign(sstr, key); err != nil { + return "", err + } + return strings.Join([]string{sstr, sig}, "."), nil +} + +// SigningString generates the signing string. This is the +// most expensive part of the whole deal. Unless you +// need this for something special, just go straight for +// the SignedString. +func (t *Token) SigningString() (string, error) { + var err error + var jsonValue []byte + + if jsonValue, err = json.Marshal(t.Header); err != nil { + return "", err + } + header := EncodeSegment(jsonValue) + + if jsonValue, err = json.Marshal(t.Claims); err != nil { + return "", err + } + claim := EncodeSegment(jsonValue) + + return strings.Join([]string{header, claim}, "."), nil +} + +// Parse parses, validates, verifies the signature and returns the parsed token. +// keyFunc will receive the parsed token and should return the cryptographic key +// for verifying the signature. +// The caller is strongly encouraged to set the WithValidMethods option to +// validate the 'alg' claim in the token matches the expected algorithm. +// For more details about the importance of validating the 'alg' claim, +// see https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ +func Parse(tokenString string, keyFunc Keyfunc, options ...ParserOption) (*Token, error) { + return NewParser(options...).Parse(tokenString, keyFunc) +} + +// ParseWithClaims is a shortcut for NewParser().ParseWithClaims(). +// +// Note: If you provide a custom claim implementation that embeds one of the standard claims (such as RegisteredClaims), +// make sure that a) you either embed a non-pointer version of the claims or b) if you are using a pointer, allocate the +// proper memory for it before passing in the overall claims, otherwise you might run into a panic. +func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc, options ...ParserOption) (*Token, error) { + return NewParser(options...).ParseWithClaims(tokenString, claims, keyFunc) +} + +// EncodeSegment encodes a JWT specific base64url encoding with padding stripped +// +// Deprecated: In a future release, we will demote this function to a non-exported function, since it +// should only be used internally +func EncodeSegment(seg []byte) string { + return base64.RawURLEncoding.EncodeToString(seg) +} + +// DecodeSegment decodes a JWT specific base64url encoding with padding stripped +// +// Deprecated: In a future release, we will demote this function to a non-exported function, since it +// should only be used internally +func DecodeSegment(seg string) ([]byte, error) { + encoding := base64.RawURLEncoding + + if DecodePaddingAllowed { + if l := len(seg) % 4; l > 0 { + seg += strings.Repeat("=", 4-l) + } + encoding = base64.URLEncoding + } + + if DecodeStrict { + encoding = encoding.Strict() + } + return encoding.DecodeString(seg) +} diff --git a/vendor/github.com/golang-jwt/jwt/v4/types.go b/vendor/github.com/golang-jwt/jwt/v4/types.go new file mode 100644 index 00000000..ac8e140e --- /dev/null +++ b/vendor/github.com/golang-jwt/jwt/v4/types.go @@ -0,0 +1,145 @@ +package jwt + +import ( + "encoding/json" + "fmt" + "math" + "reflect" + "strconv" + "time" +) + +// TimePrecision sets the precision of times and dates within this library. +// This has an influence on the precision of times when comparing expiry or +// other related time fields. Furthermore, it is also the precision of times +// when serializing. +// +// For backwards compatibility the default precision is set to seconds, so that +// no fractional timestamps are generated. +var TimePrecision = time.Second + +// MarshalSingleStringAsArray modifies the behaviour of the ClaimStrings type, especially +// its MarshalJSON function. +// +// If it is set to true (the default), it will always serialize the type as an +// array of strings, even if it just contains one element, defaulting to the behaviour +// of the underlying []string. If it is set to false, it will serialize to a single +// string, if it contains one element. Otherwise, it will serialize to an array of strings. +var MarshalSingleStringAsArray = true + +// NumericDate represents a JSON numeric date value, as referenced at +// https://datatracker.ietf.org/doc/html/rfc7519#section-2. +type NumericDate struct { + time.Time +} + +// NewNumericDate constructs a new *NumericDate from a standard library time.Time struct. +// It will truncate the timestamp according to the precision specified in TimePrecision. +func NewNumericDate(t time.Time) *NumericDate { + return &NumericDate{t.Truncate(TimePrecision)} +} + +// newNumericDateFromSeconds creates a new *NumericDate out of a float64 representing a +// UNIX epoch with the float fraction representing non-integer seconds. +func newNumericDateFromSeconds(f float64) *NumericDate { + round, frac := math.Modf(f) + return NewNumericDate(time.Unix(int64(round), int64(frac*1e9))) +} + +// MarshalJSON is an implementation of the json.RawMessage interface and serializes the UNIX epoch +// represented in NumericDate to a byte array, using the precision specified in TimePrecision. +func (date NumericDate) MarshalJSON() (b []byte, err error) { + var prec int + if TimePrecision < time.Second { + prec = int(math.Log10(float64(time.Second) / float64(TimePrecision))) + } + truncatedDate := date.Truncate(TimePrecision) + + // For very large timestamps, UnixNano would overflow an int64, but this + // function requires nanosecond level precision, so we have to use the + // following technique to get round the issue: + // 1. Take the normal unix timestamp to form the whole number part of the + // output, + // 2. Take the result of the Nanosecond function, which retuns the offset + // within the second of the particular unix time instance, to form the + // decimal part of the output + // 3. Concatenate them to produce the final result + seconds := strconv.FormatInt(truncatedDate.Unix(), 10) + nanosecondsOffset := strconv.FormatFloat(float64(truncatedDate.Nanosecond())/float64(time.Second), 'f', prec, 64) + + output := append([]byte(seconds), []byte(nanosecondsOffset)[1:]...) + + return output, nil +} + +// UnmarshalJSON is an implementation of the json.RawMessage interface and deserializses a +// NumericDate from a JSON representation, i.e. a json.Number. This number represents an UNIX epoch +// with either integer or non-integer seconds. +func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { + var ( + number json.Number + f float64 + ) + + if err = json.Unmarshal(b, &number); err != nil { + return fmt.Errorf("could not parse NumericData: %w", err) + } + + if f, err = number.Float64(); err != nil { + return fmt.Errorf("could not convert json number value to float: %w", err) + } + + n := newNumericDateFromSeconds(f) + *date = *n + + return nil +} + +// ClaimStrings is basically just a slice of strings, but it can be either serialized from a string array or just a string. +// This type is necessary, since the "aud" claim can either be a single string or an array. +type ClaimStrings []string + +func (s *ClaimStrings) UnmarshalJSON(data []byte) (err error) { + var value interface{} + + if err = json.Unmarshal(data, &value); err != nil { + return err + } + + var aud []string + + switch v := value.(type) { + case string: + aud = append(aud, v) + case []string: + aud = ClaimStrings(v) + case []interface{}: + for _, vv := range v { + vs, ok := vv.(string) + if !ok { + return &json.UnsupportedTypeError{Type: reflect.TypeOf(vv)} + } + aud = append(aud, vs) + } + case nil: + return nil + default: + return &json.UnsupportedTypeError{Type: reflect.TypeOf(v)} + } + + *s = aud + + return +} + +func (s ClaimStrings) MarshalJSON() (b []byte, err error) { + // This handles a special case in the JWT RFC. If the string array, e.g. used by the "aud" field, + // only contains one element, it MAY be serialized as a single string. This may or may not be + // desired based on the ecosystem of other JWT library used, so we make it configurable by the + // variable MarshalSingleStringAsArray. + if len(s) == 1 && !MarshalSingleStringAsArray { + return json.Marshal(s[0]) + } + + return json.Marshal([]string(s)) +} diff --git a/vendor/github.com/golang-sql/civil/civil.go b/vendor/github.com/golang-sql/civil/civil.go index 29272ef2..a3924680 100644 --- a/vendor/github.com/golang-sql/civil/civil.go +++ b/vendor/github.com/golang-sql/civil/civil.go @@ -107,6 +107,11 @@ func (d1 Date) After(d2 Date) bool { return d2.Before(d1) } +// IsZero reports whether date fields are set to their default value. +func (d Date) IsZero() bool { + return (d.Year == 0) && (int(d.Month) == 0) && (d.Day == 0) +} + // MarshalText implements the encoding.TextMarshaler interface. // The output is the result of d.String(). func (d Date) MarshalText() ([]byte, error) { @@ -175,6 +180,11 @@ func (t Time) IsValid() bool { return TimeOf(tm) == t } +// IsZero reports whether time fields are set to their default value. +func (t Time) IsZero() bool { + return (t.Hour == 0) && (t.Minute == 0) && (t.Second == 0) && (t.Nanosecond == 0) +} + // MarshalText implements the encoding.TextMarshaler interface. // The output is the result of t.String(). func (t Time) MarshalText() ([]byte, error) { @@ -262,6 +272,11 @@ func (dt1 DateTime) After(dt2 DateTime) bool { return dt2.Before(dt1) } +// IsZero reports whether datetime fields are set to their default value. +func (dt DateTime) IsZero() bool { + return dt.Date.IsZero() && dt.Time.IsZero() +} + // MarshalText implements the encoding.TextMarshaler interface. // The output is the result of dt.String(). func (dt DateTime) MarshalText() ([]byte, error) { diff --git a/vendor/github.com/golang-sql/sqlexp/LICENSE b/vendor/github.com/golang-sql/sqlexp/LICENSE new file mode 100644 index 00000000..32017f8f --- /dev/null +++ b/vendor/github.com/golang-sql/sqlexp/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2017 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/golang-sql/sqlexp/PATENTS b/vendor/github.com/golang-sql/sqlexp/PATENTS new file mode 100644 index 00000000..73309904 --- /dev/null +++ b/vendor/github.com/golang-sql/sqlexp/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/github.com/golang-sql/sqlexp/README.md b/vendor/github.com/golang-sql/sqlexp/README.md new file mode 100644 index 00000000..f5d123fc --- /dev/null +++ b/vendor/github.com/golang-sql/sqlexp/README.md @@ -0,0 +1,5 @@ +# golang-sql exp + +https://godoc.org/github.com/golang-sql/sqlexp + +All contributions must have a valid golang CLA. diff --git a/vendor/github.com/golang-sql/sqlexp/doc.go b/vendor/github.com/golang-sql/sqlexp/doc.go new file mode 100644 index 00000000..a1f5f692 --- /dev/null +++ b/vendor/github.com/golang-sql/sqlexp/doc.go @@ -0,0 +1,8 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package sqlexp provides interfaces and functions that may be adopted into +// the database/sql package in the future. All features may change or be removed +// in the future. +package sqlexp // imports github.com/golang-sql/sqlexp diff --git a/vendor/github.com/golang-sql/sqlexp/messages.go b/vendor/github.com/golang-sql/sqlexp/messages.go new file mode 100644 index 00000000..ae57b3cd --- /dev/null +++ b/vendor/github.com/golang-sql/sqlexp/messages.go @@ -0,0 +1,80 @@ +package sqlexp + +import ( + "context" + "fmt" +) + +// RawMessage is returned from RowsMessage. +type RawMessage interface{} + +// ReturnMessage may be passed into a Query argument. +// +// Drivers must implement driver.NamedValueChecker, +// call ReturnMessageInit on it, save it internally, +// and return driver.ErrOmitArgument to prevent +// this from appearing in the query arguments. +// +// Queries that recieve this message should also not return +// SQL errors from the Query method, but wait to return +// it in a Message. +type ReturnMessage struct { + queue chan RawMessage +} + +// Message is called by clients after Query to dequeue messages. +func (m *ReturnMessage) Message(ctx context.Context) RawMessage { + select { + case <-ctx.Done(): + return MsgNextResultSet{} + case raw := <-m.queue: + return raw + } +} + +// ReturnMessageEnqueue is called by the driver to enqueue the driver. +// Drivers should not call this until after it returns from Query. +func ReturnMessageEnqueue(ctx context.Context, m *ReturnMessage, raw RawMessage) error { + select { + case <-ctx.Done(): + return ctx.Err() + case m.queue <- raw: + return nil + } +} + +// ReturnMessageInit is called by database/sql setup the ReturnMessage internals. +func ReturnMessageInit(m *ReturnMessage) { + m.queue = make(chan RawMessage, 15) +} + +type ( + // MsgNextResultSet must be checked for. When received, NextResultSet + // should be called and if false the message loop should be exited. + MsgNextResultSet struct{} + + // MsgNext indicates the result set ready to be scanned. + // This message will often be followed with: + // + // for rows.Next() { + // rows.Scan(&v) + // } + MsgNext struct{} + + // MsgRowsAffected returns the number of rows affected. + // Not all operations that affect rows return results, thus this message + // may be received multiple times. + MsgRowsAffected struct{ Count int64 } + + // MsgLastInsertID returns the value of last inserted row. For many + // database systems and tables this will return int64. Some databases + // may return a string or GUID equivalent. + MsgLastInsertID struct{ Value interface{} } + + // MsgNotice is raised from the SQL text and is only informational. + MsgNotice struct{ Message fmt.Stringer } + + // MsgError returns SQL errors from the database system (not transport + // or other system level errors). + MsgError struct{ Error error } +) diff --git a/vendor/github.com/golang-sql/sqlexp/mssql.go b/vendor/github.com/golang-sql/sqlexp/mssql.go new file mode 100644 index 00000000..3e11547b --- /dev/null +++ b/vendor/github.com/golang-sql/sqlexp/mssql.go @@ -0,0 +1,73 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sqlexp + +import ( + "context" + "database/sql/driver" + "fmt" + "strings" +) + +type mssql struct{} + +var ( + _ DriverNamer = mssql{} + _ DriverQuoter = mssql{} + _ DriverSavepointer = mssql{} +) + +func (mssql) Open(string) (driver.Conn, error) { + panic("not implemented") +} + +func (mssql) Namer(ctx context.Context) (Namer, error) { + return sqlServerNamer{}, nil +} + +func (mssql) Quoter(ctx context.Context) (Quoter, error) { + return sqlServerQuoter{}, nil +} + +func (mssql) Savepointer() (Savepointer, error) { + return sqlServerSavepointer{}, nil +} + +type sqlServerNamer struct{} + +func (sqlServerNamer) Name() string { + return "sqlserver" +} +func (sqlServerNamer) Dialect() string { + return DialectTSQL +} + +type sqlServerQuoter struct{} + +func (sqlServerQuoter) ID(name string) string { + return "[" + strings.Replace(name, "]", "]]", -1) + "]" +} +func (sqlServerQuoter) Value(v interface{}) string { + switch v := v.(type) { + default: + panic("unsupported value") + case string: + return "'" + strings.Replace(v, "'", "''", -1) + "'" + } +} + +type sqlServerSavepointer struct{} + +func (sqlServerSavepointer) Release(name string) string { + return "" +} + +func (sqlServerSavepointer) Create(name string) string { + return fmt.Sprintf("save tran %s;", name) +} + +func (sqlServerSavepointer) Rollback(name string) string { + return fmt.Sprintf("rollback tran %s;", name) +} diff --git a/vendor/github.com/golang-sql/sqlexp/namer.go b/vendor/github.com/golang-sql/sqlexp/namer.go new file mode 100644 index 00000000..99ea124f --- /dev/null +++ b/vendor/github.com/golang-sql/sqlexp/namer.go @@ -0,0 +1,59 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sqlexp + +import ( + "context" + "database/sql/driver" + "errors" + "reflect" +) + +const ( + DialectPostgres = "postgres" + DialectTSQL = "tsql" + DialectMySQL = "mysql" + DialectSQLite = "sqlite" + DialectOracle = "oracle" +) + +// Namer returns the name of the database and the SQL dialect it +// uses. +type Namer interface { + // Name of the database management system. + // + // Examples: + // "posgresql-9.6" + // "sqlserver-10.54.32" + // "cockroachdb-1.0" + Name() string + + // Dialect of SQL used in the database. + Dialect() string +} + +// DriverNamer may be implemented on the driver.Driver interface. +// It may need to request information from the server to return +// the correct information. +type DriverNamer interface { + Namer(ctx context.Context) (Namer, error) +} + +// NamerFromDriver returns the DriverNamer from the DB if +// it is implemented. +func NamerFromDriver(d driver.Driver, ctx context.Context) (Namer, error) { + if q, is := d.(DriverNamer); is { + return q.Namer(ctx) + } + dv := reflect.ValueOf(d) + + d, found := internalDrivers[dv.Type().String()] + if found { + if q, is := d.(DriverNamer); is { + return q.Namer(ctx) + } + } + return nil, errors.New("namer not found") +} diff --git a/vendor/github.com/golang-sql/sqlexp/pg.go b/vendor/github.com/golang-sql/sqlexp/pg.go new file mode 100644 index 00000000..16a6a468 --- /dev/null +++ b/vendor/github.com/golang-sql/sqlexp/pg.go @@ -0,0 +1,67 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sqlexp + +import ( + "context" + "database/sql/driver" + "fmt" +) + +type postgresql struct{} + +var ( + _ DriverNamer = postgresql{} + _ DriverQuoter = postgresql{} + _ DriverSavepointer = postgresql{} +) + +func (postgresql) Open(string) (driver.Conn, error) { + panic("not implemented") +} + +func (postgresql) Namer(ctx context.Context) (Namer, error) { + return pgNamer{}, nil +} + +func (postgresql) Quoter(ctx context.Context) (Quoter, error) { + panic("not implemented") +} + +func (postgresql) Savepointer() (Savepointer, error) { + return pgSavepointer{}, nil +} + +type pgNamer struct{} + +func (pgNamer) Name() string { + return "postgresql" +} +func (pgNamer) Dialect() string { + return DialectPostgres +} + +type pgQuoter struct{} + +func (pgQuoter) ID(name string) string { + return "" +} +func (pgQuoter) Value(v interface{}) string { + return "" +} + +type pgSavepointer struct{} + +func (pgSavepointer) Release(name string) string { + return fmt.Sprintf("release savepoint %s;", name) +} + +func (pgSavepointer) Create(name string) string { + return fmt.Sprintf("savepoint %s;", name) +} + +func (pgSavepointer) Rollback(name string) string { + return fmt.Sprintf("rollback to savepoint %s;", name) +} diff --git a/vendor/github.com/golang-sql/sqlexp/querier.go b/vendor/github.com/golang-sql/sqlexp/querier.go new file mode 100644 index 00000000..e09f9ab4 --- /dev/null +++ b/vendor/github.com/golang-sql/sqlexp/querier.go @@ -0,0 +1,22 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sqlexp + +import ( + "context" + "database/sql" +) + +// Querier is the common interface to execute queries on a DB, Tx, or Conn. +type Querier interface { + ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) + QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) + QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row +} + +var ( + _ Querier = &sql.DB{} + _ Querier = &sql.Tx{} +) diff --git a/vendor/github.com/golang-sql/sqlexp/quoter.go b/vendor/github.com/golang-sql/sqlexp/quoter.go new file mode 100644 index 00000000..355ed31e --- /dev/null +++ b/vendor/github.com/golang-sql/sqlexp/quoter.go @@ -0,0 +1,57 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sqlexp + +import ( + "context" + "database/sql/driver" + "errors" + "reflect" +) + +// BUG(kardianos): Both the Quoter and Namer may need to access the server. + +// Quoter returns safe and valid SQL strings to use when building a SQL text. +type Quoter interface { + // ID quotes identifiers such as schema, table, or column names. + // ID does not operate on multipart identifiers such as "public.Table", + // it only operates on single identifiers such as "public" and "Table". + ID(name string) string + + // Value quotes database values such as string or []byte types as strings + // that are suitable and safe to embed in SQL text. The returned value + // of a string will include all surrounding quotes. + // + // If a value type is not supported it must panic. + Value(v interface{}) string +} + +// DriverQuoter returns a Quoter interface and is suitable for extending +// the driver.Driver type. +// +// The driver may need to hit the database to determine how it is configured to +// ensure the correct escaping rules are used. +type DriverQuoter interface { + Quoter(ctx context.Context) (Quoter, error) +} + +// QuoterFromDriver takes a database driver, often obtained through a sql.DB.Driver +// call or from using it directly to get the quoter interface. +// +// Currently MssqlDriver is hard-coded to also return a valided Quoter. +func QuoterFromDriver(d driver.Driver, ctx context.Context) (Quoter, error) { + if q, is := d.(DriverQuoter); is { + return q.Quoter(ctx) + } + dv := reflect.ValueOf(d) + + d, found := internalDrivers[dv.Type().String()] + if found { + if q, is := d.(DriverQuoter); is { + return q.Quoter(ctx) + } + } + return nil, errors.New("quoter interface not found") +} diff --git a/vendor/github.com/golang-sql/sqlexp/registry.go b/vendor/github.com/golang-sql/sqlexp/registry.go new file mode 100644 index 00000000..474e8a6b --- /dev/null +++ b/vendor/github.com/golang-sql/sqlexp/registry.go @@ -0,0 +1,15 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sqlexp + +import ( + "database/sql/driver" +) + +var internalDrivers = map[string]driver.Driver{ + "*mssql.MssqlDriver": mssql{}, + "*pq.Driver": postgresql{}, + "*stdlib.Driver": postgresql{}, +} diff --git a/vendor/github.com/golang-sql/sqlexp/savepoint.go b/vendor/github.com/golang-sql/sqlexp/savepoint.go new file mode 100644 index 00000000..f27eb991 --- /dev/null +++ b/vendor/github.com/golang-sql/sqlexp/savepoint.go @@ -0,0 +1,37 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sqlexp + +import ( + "database/sql/driver" + "errors" + "reflect" +) + +type Savepointer interface { + Release(name string) string + Create(name string) string + Rollback(name string) string +} + +type DriverSavepointer interface { + Savepointer() (Savepointer, error) +} + +// SavepointFromDriver +func SavepointFromDriver(d driver.Driver) (Savepointer, error) { + if q, is := d.(DriverSavepointer); is { + return q.Savepointer() + } + dv := reflect.ValueOf(d) + + d, found := internalDrivers[dv.Type().String()] + if found { + if q, is := d.(DriverSavepointer); is { + return q.Savepointer() + } + } + return nil, errors.New("savepointer interface not found") +} diff --git a/vendor/github.com/golang/glog/README b/vendor/github.com/golang/glog/README deleted file mode 100644 index 387b4eb6..00000000 --- a/vendor/github.com/golang/glog/README +++ /dev/null @@ -1,44 +0,0 @@ -glog -==== - -Leveled execution logs for Go. - -This is an efficient pure Go implementation of leveled logs in the -manner of the open source C++ package - https://github.com/google/glog - -By binding methods to booleans it is possible to use the log package -without paying the expense of evaluating the arguments to the log. -Through the -vmodule flag, the package also provides fine-grained -control over logging at the file level. - -The comment from glog.go introduces the ideas: - - Package glog implements logging analogous to the Google-internal - C++ INFO/ERROR/V setup. It provides functions Info, Warning, - Error, Fatal, plus formatting variants such as Infof. It - also provides V-style logging controlled by the -v and - -vmodule=file=2 flags. - - Basic examples: - - glog.Info("Prepare to repel boarders") - - glog.Fatalf("Initialization failed: %s", err) - - See the documentation for the V function for an explanation - of these examples: - - if glog.V(2) { - glog.Info("Starting transaction...") - } - - glog.V(2).Infoln("Processed", nItems, "elements") - - -The repository contains an open source version of the log package -used inside Google. The master copy of the source lives inside -Google, not here. The code in this repo is for export only and is not itself -under development. Feature requests will be ignored. - -Send bug reports to golang-nuts@googlegroups.com. diff --git a/vendor/github.com/golang/glog/README.md b/vendor/github.com/golang/glog/README.md new file mode 100644 index 00000000..a4f73883 --- /dev/null +++ b/vendor/github.com/golang/glog/README.md @@ -0,0 +1,36 @@ +# glog + +[![PkgGoDev](https://pkg.go.dev/badge/github.com/golang/glog)](https://pkg.go.dev/github.com/golang/glog) + +Leveled execution logs for Go. + +This is an efficient pure Go implementation of leveled logs in the +manner of the open source C++ package [_glog_](https://github.com/google/glog). + +By binding methods to booleans it is possible to use the log package without paying the expense of evaluating the arguments to the log. Through the `-vmodule` flag, the package also provides fine-grained +control over logging at the file level. + +The comment from `glog.go` introduces the ideas: + +Package _glog_ implements logging analogous to the Google-internal C++ INFO/ERROR/V setup. It provides the functions Info, Warning, Error, Fatal, plus formatting variants such as Infof. It also provides V-style loggingcontrolled by the `-v` and `-vmodule=file=2` flags. + +Basic examples: + +```go +glog.Info("Prepare to repel boarders") + +glog.Fatalf("Initialization failed: %s", err) +``` + +See the documentation for the V function for an explanation of these examples: + +```go +if glog.V(2) { + glog.Info("Starting transaction...") +} +glog.V(2).Infoln("Processed", nItems, "elements") +``` + +The repository contains an open source version of the log package used inside Google. The master copy of the source lives inside Google, not here. The code in this repo is for export only and is not itself under development. Feature requests will be ignored. + +Send bug reports to golang-nuts@googlegroups.com. diff --git a/vendor/github.com/golang/glog/glog.go b/vendor/github.com/golang/glog/glog.go index 54bd7afd..8c00e737 100644 --- a/vendor/github.com/golang/glog/glog.go +++ b/vendor/github.com/golang/glog/glog.go @@ -1,6 +1,6 @@ -// Go support for leveled logs, analogous to https://code.google.com/p/google-glog/ +// Go support for leveled logs, analogous to https://github.com/google/glog. // -// Copyright 2013 Google Inc. All Rights Reserved. +// Copyright 2023 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,8 +15,26 @@ // limitations under the License. // Package glog implements logging analogous to the Google-internal C++ INFO/ERROR/V setup. -// It provides functions Info, Warning, Error, Fatal, plus formatting variants such as -// Infof. It also provides V-style logging controlled by the -v and -vmodule=file=2 flags. +// It provides functions that have a name matched by regex: +// +// (Info|Warning|Error|Fatal)(Context)?(Depth)?(f)? +// +// If Context is present, function takes context.Context argument. The +// context is used to pass through the Trace Context to log sinks that can make use +// of it. +// It is recommended to use the context variant of the functions over the non-context +// variants if a context is available to make sure the Trace Contexts are present +// in logs. +// +// If Depth is present, this function calls log from a different depth in the call stack. +// This enables a callee to emit logs that use the callsite information of its caller +// or any other callers in the stack. When depth == 0, the original callee's line +// information is emitted. When depth > 0, depth frames are skipped in the call stack +// and the final frame is treated like the original callee to Info. +// +// If 'f' is present, function formats according to a format specifier. +// +// This package also provides V-style logging controlled by the -v and -vmodule=file=2 flags. // // Basic examples: // @@ -50,15 +68,15 @@ // Log files will be written to this directory instead of the // default temporary directory. // -// Other flags provide aids to debugging. +// Other flags provide aids to debugging. // // -log_backtrace_at="" -// When set to a file and line number holding a logging statement, -// such as +// A comma-separated list of file and line numbers holding a logging +// statement, such as // -log_backtrace_at=gopherflakes.go:234 -// a stack trace will be written to the Info log whenever execution -// hits that statement. (Unlike with -vmodule, the ".go" must be -// present.) +// A stack trace will be written to the Info log whenever execution +// hits one of these statements. (Unlike with -vmodule, the ".go" +// must bepresent.) // -v=0 // Enable V-leveled logging at the specified level. // -vmodule="" @@ -66,100 +84,47 @@ // where pattern is a literal file name (minus the ".go" suffix) or // "glob" pattern and N is a V level. For instance, // -vmodule=gopher*=3 -// sets the V level to 3 in all Go files whose names begin "gopher". -// +// sets the V level to 3 in all Go files whose names begin with "gopher", +// and +// -vmodule=/path/to/glog/glog_test=1 +// sets the V level to 1 in the Go file /path/to/glog/glog_test.go. +// If a glob pattern contains a slash, it is matched against the full path, +// and the file name. Otherwise, the pattern is +// matched only against the file's basename. When both -vmodule and -v +// are specified, the -vmodule values take precedence for the specified +// modules. package glog +// This file contains the parts of the log package that are shared among all +// implementations (file, envelope, and appengine). + import ( - "bufio" "bytes" + "context" "errors" - "flag" "fmt" - "io" stdLog "log" "os" - "path/filepath" + "reflect" "runtime" + "runtime/pprof" "strconv" - "strings" "sync" "sync/atomic" "time" + + "github.com/golang/glog/internal/logsink" + "github.com/golang/glog/internal/stackdump" ) -// severity identifies the sort of log: info, warning etc. It also implements -// the flag.Value interface. The -stderrthreshold flag is of type severity and -// should be modified only through the flag.Value interface. The values match -// the corresponding constants in C++. -type severity int32 // sync/atomic int32 +var timeNow = time.Now // Stubbed out for testing. -// These constants identify the log levels in order of increasing severity. -// A message written to a high-severity log file is also written to each -// lower-severity log file. -const ( - infoLog severity = iota - warningLog - errorLog - fatalLog - numSeverity = 4 -) +// MaxSize is the maximum size of a log file in bytes. +var MaxSize uint64 = 1024 * 1024 * 1800 -const severityChar = "IWEF" - -var severityName = []string{ - infoLog: "INFO", - warningLog: "WARNING", - errorLog: "ERROR", - fatalLog: "FATAL", -} - -// get returns the value of the severity. -func (s *severity) get() severity { - return severity(atomic.LoadInt32((*int32)(s))) -} - -// set sets the value of the severity. -func (s *severity) set(val severity) { - atomic.StoreInt32((*int32)(s), int32(val)) -} - -// String is part of the flag.Value interface. -func (s *severity) String() string { - return strconv.FormatInt(int64(*s), 10) -} - -// Get is part of the flag.Value interface. -func (s *severity) Get() interface{} { - return *s -} - -// Set is part of the flag.Value interface. -func (s *severity) Set(value string) error { - var threshold severity - // Is it a known name? - if v, ok := severityByName(value); ok { - threshold = v - } else { - v, err := strconv.Atoi(value) - if err != nil { - return err - } - threshold = severity(v) - } - logging.stderrThreshold.set(threshold) - return nil -} - -func severityByName(s string) (severity, bool) { - s = strings.ToUpper(s) - for i, name := range severityName { - if name == s { - return severity(i), true - } - } - return 0, false -} +// ErrNoLog is the error we return if no log file has yet been created +// for the specified log type. +var ErrNoLog = errors.New("log file not yet created") // OutputStats tracks the number of output lines and bytes written. type OutputStats struct { @@ -183,724 +148,108 @@ var Stats struct { Info, Warning, Error OutputStats } -var severityStats = [numSeverity]*OutputStats{ - infoLog: &Stats.Info, - warningLog: &Stats.Warning, - errorLog: &Stats.Error, +var severityStats = [...]*OutputStats{ + logsink.Info: &Stats.Info, + logsink.Warning: &Stats.Warning, + logsink.Error: &Stats.Error, + logsink.Fatal: nil, } -// Level is exported because it appears in the arguments to V and is -// the type of the v flag, which can be set programmatically. -// It's a distinct type because we want to discriminate it from logType. -// Variables of type level are only changed under logging.mu. -// The -v flag is read only with atomic ops, so the state of the logging -// module is consistent. - -// Level is treated as a sync/atomic int32. - -// Level specifies a level of verbosity for V logs. *Level implements -// flag.Value; the -v flag is of type Level and should be modified -// only through the flag.Value interface. +// Level specifies a level of verbosity for V logs. The -v flag is of type +// Level and should be modified only through the flag.Value interface. type Level int32 -// get returns the value of the Level. -func (l *Level) get() Level { - return Level(atomic.LoadInt32((*int32)(l))) -} +var metaPool sync.Pool // Pool of *logsink.Meta. -// set sets the value of the Level. -func (l *Level) set(val Level) { - atomic.StoreInt32((*int32)(l), int32(val)) -} - -// String is part of the flag.Value interface. -func (l *Level) String() string { - return strconv.FormatInt(int64(*l), 10) -} - -// Get is part of the flag.Value interface. -func (l *Level) Get() interface{} { - return *l -} - -// Set is part of the flag.Value interface. -func (l *Level) Set(value string) error { - v, err := strconv.Atoi(value) - if err != nil { - return err +// metaPoolGet returns a *logsink.Meta from metaPool as both an interface and a +// pointer, allocating a new one if necessary. (Returning the interface value +// directly avoids an allocation if there was an existing pointer in the pool.) +func metaPoolGet() (any, *logsink.Meta) { + if metai := metaPool.Get(); metai != nil { + return metai, metai.(*logsink.Meta) } - logging.mu.Lock() - defer logging.mu.Unlock() - logging.setVState(Level(v), logging.vmodule.filter, false) - return nil + meta := new(logsink.Meta) + return meta, meta } -// moduleSpec represents the setting of the -vmodule flag. -type moduleSpec struct { - filter []modulePat +type stack bool + +const ( + noStack = stack(false) + withStack = stack(true) +) + +func appendBacktrace(depth int, format string, args []any) (string, []any) { + // Capture a backtrace as a stackdump.Stack (both text and PC slice). + // Structured log sinks can extract the backtrace in whichever format they + // prefer (PCs or text), and Text sinks will include it as just another part + // of the log message. + // + // Use depth instead of depth+1 so that the backtrace always includes the + // log function itself - otherwise the reason for the trace appearing in the + // log may not be obvious to the reader. + dump := stackdump.Caller(depth) + + // Add an arg and an entry in the format string for the stack dump. + // + // Copy the "args" slice to avoid a rare but serious aliasing bug + // (corrupting the caller's slice if they passed it to a non-Fatal call + // using "..."). + format = format + "\n\n%v\n" + args = append(append([]any(nil), args...), dump) + + return format, args } -// modulePat contains a filter for the -vmodule flag. -// It holds a verbosity level and a file pattern to match. -type modulePat struct { - pattern string - literal bool // The pattern is a literal string - level Level +// logf acts as ctxlogf, but doesn't expect a context. +func logf(depth int, severity logsink.Severity, verbose bool, stack stack, format string, args ...any) { + ctxlogf(nil, depth+1, severity, verbose, stack, format, args...) } -// match reports whether the file matches the pattern. It uses a string -// comparison if the pattern contains no metacharacters. -func (m *modulePat) match(file string) bool { - if m.literal { - return file == m.pattern - } - match, _ := filepath.Match(m.pattern, file) - return match -} - -func (m *moduleSpec) String() string { - // Lock because the type is not atomic. TODO: clean this up. - logging.mu.Lock() - defer logging.mu.Unlock() - var b bytes.Buffer - for i, f := range m.filter { - if i > 0 { - b.WriteRune(',') - } - fmt.Fprintf(&b, "%s=%d", f.pattern, f.level) - } - return b.String() -} - -// Get is part of the (Go 1.2) flag.Getter interface. It always returns nil for this flag type since the -// struct is not exported. -func (m *moduleSpec) Get() interface{} { - return nil -} - -var errVmoduleSyntax = errors.New("syntax error: expect comma-separated list of filename=N") - -// Syntax: -vmodule=recordio=2,file=1,gfs*=3 -func (m *moduleSpec) Set(value string) error { - var filter []modulePat - for _, pat := range strings.Split(value, ",") { - if len(pat) == 0 { - // Empty strings such as from a trailing comma can be ignored. - continue - } - patLev := strings.Split(pat, "=") - if len(patLev) != 2 || len(patLev[0]) == 0 || len(patLev[1]) == 0 { - return errVmoduleSyntax - } - pattern := patLev[0] - v, err := strconv.Atoi(patLev[1]) - if err != nil { - return errors.New("syntax error: expect comma-separated list of filename=N") - } - if v < 0 { - return errors.New("negative value for vmodule level") - } - if v == 0 { - continue // Ignore. It's harmless but no point in paying the overhead. - } - // TODO: check syntax of filter? - filter = append(filter, modulePat{pattern, isLiteral(pattern), Level(v)}) - } - logging.mu.Lock() - defer logging.mu.Unlock() - logging.setVState(logging.verbosity, filter, true) - return nil -} - -// isLiteral reports whether the pattern is a literal string, that is, has no metacharacters -// that require filepath.Match to be called to match the pattern. -func isLiteral(pattern string) bool { - return !strings.ContainsAny(pattern, `\*?[]`) -} - -// traceLocation represents the setting of the -log_backtrace_at flag. -type traceLocation struct { - file string - line int -} - -// isSet reports whether the trace location has been specified. -// logging.mu is held. -func (t *traceLocation) isSet() bool { - return t.line > 0 -} - -// match reports whether the specified file and line matches the trace location. -// The argument file name is the full path, not the basename specified in the flag. -// logging.mu is held. -func (t *traceLocation) match(file string, line int) bool { - if t.line != line { - return false - } - if i := strings.LastIndex(file, "/"); i >= 0 { - file = file[i+1:] - } - return t.file == file -} - -func (t *traceLocation) String() string { - // Lock because the type is not atomic. TODO: clean this up. - logging.mu.Lock() - defer logging.mu.Unlock() - return fmt.Sprintf("%s:%d", t.file, t.line) -} - -// Get is part of the (Go 1.2) flag.Getter interface. It always returns nil for this flag type since the -// struct is not exported -func (t *traceLocation) Get() interface{} { - return nil -} - -var errTraceSyntax = errors.New("syntax error: expect file.go:234") - -// Syntax: -log_backtrace_at=gopherflakes.go:234 -// Note that unlike vmodule the file extension is included here. -func (t *traceLocation) Set(value string) error { - if value == "" { - // Unset. - t.line = 0 - t.file = "" - } - fields := strings.Split(value, ":") - if len(fields) != 2 { - return errTraceSyntax - } - file, line := fields[0], fields[1] - if !strings.Contains(file, ".") { - return errTraceSyntax - } - v, err := strconv.Atoi(line) - if err != nil { - return errTraceSyntax - } - if v <= 0 { - return errors.New("negative or zero value for level") - } - logging.mu.Lock() - defer logging.mu.Unlock() - t.line = v - t.file = file - return nil -} - -// flushSyncWriter is the interface satisfied by logging destinations. -type flushSyncWriter interface { - Flush() error - Sync() error - io.Writer -} - -func init() { - flag.BoolVar(&logging.toStderr, "logtostderr", false, "log to standard error instead of files") - flag.BoolVar(&logging.alsoToStderr, "alsologtostderr", false, "log to standard error as well as files") - flag.Var(&logging.verbosity, "v", "log level for V logs") - flag.Var(&logging.stderrThreshold, "stderrthreshold", "logs at or above this threshold go to stderr") - flag.Var(&logging.vmodule, "vmodule", "comma-separated list of pattern=N settings for file-filtered logging") - flag.Var(&logging.traceLocation, "log_backtrace_at", "when logging hits line file:N, emit a stack trace") - - // Default stderrThreshold is ERROR. - logging.stderrThreshold = errorLog - - logging.setVState(0, nil, false) - go logging.flushDaemon() -} - -// Flush flushes all pending log I/O. -func Flush() { - logging.lockAndFlushAll() -} - -// loggingT collects all the global state of the logging setup. -type loggingT struct { - // Boolean flags. Not handled atomically because the flag.Value interface - // does not let us avoid the =true, and that shorthand is necessary for - // compatibility. TODO: does this matter enough to fix? Seems unlikely. - toStderr bool // The -logtostderr flag. - alsoToStderr bool // The -alsologtostderr flag. - - // Level flag. Handled atomically. - stderrThreshold severity // The -stderrthreshold flag. - - // freeList is a list of byte buffers, maintained under freeListMu. - freeList *buffer - // freeListMu maintains the free list. It is separate from the main mutex - // so buffers can be grabbed and printed to without holding the main lock, - // for better parallelization. - freeListMu sync.Mutex - - // mu protects the remaining elements of this structure and is - // used to synchronize logging. - mu sync.Mutex - // file holds writer for each of the log types. - file [numSeverity]flushSyncWriter - // pcs is used in V to avoid an allocation when computing the caller's PC. - pcs [1]uintptr - // vmap is a cache of the V Level for each V() call site, identified by PC. - // It is wiped whenever the vmodule flag changes state. - vmap map[uintptr]Level - // filterLength stores the length of the vmodule filter chain. If greater - // than zero, it means vmodule is enabled. It may be read safely - // using sync.LoadInt32, but is only modified under mu. - filterLength int32 - // traceLocation is the state of the -log_backtrace_at flag. - traceLocation traceLocation - // These flags are modified only under lock, although verbosity may be fetched - // safely using atomic.LoadInt32. - vmodule moduleSpec // The state of the -vmodule flag. - verbosity Level // V logging level, the value of the -v flag/ -} - -// buffer holds a byte Buffer for reuse. The zero value is ready for use. -type buffer struct { - bytes.Buffer - tmp [64]byte // temporary byte array for creating headers. - next *buffer -} - -var logging loggingT - -// setVState sets a consistent state for V logging. -// l.mu is held. -func (l *loggingT) setVState(verbosity Level, filter []modulePat, setFilter bool) { - // Turn verbosity off so V will not fire while we are in transition. - logging.verbosity.set(0) - // Ditto for filter length. - atomic.StoreInt32(&logging.filterLength, 0) - - // Set the new filters and wipe the pc->Level map if the filter has changed. - if setFilter { - logging.vmodule.filter = filter - logging.vmap = make(map[uintptr]Level) - } - - // Things are consistent now, so enable filtering and verbosity. - // They are enabled in order opposite to that in V. - atomic.StoreInt32(&logging.filterLength, int32(len(filter))) - logging.verbosity.set(verbosity) -} - -// getBuffer returns a new, ready-to-use buffer. -func (l *loggingT) getBuffer() *buffer { - l.freeListMu.Lock() - b := l.freeList - if b != nil { - l.freeList = b.next - } - l.freeListMu.Unlock() - if b == nil { - b = new(buffer) - } else { - b.next = nil - b.Reset() - } - return b -} - -// putBuffer returns a buffer to the free list. -func (l *loggingT) putBuffer(b *buffer) { - if b.Len() >= 256 { - // Let big buffers die a natural death. - return - } - l.freeListMu.Lock() - b.next = l.freeList - l.freeList = b - l.freeListMu.Unlock() -} - -var timeNow = time.Now // Stubbed out for testing. - -/* -header formats a log header as defined by the C++ implementation. -It returns a buffer containing the formatted header and the user's file and line number. -The depth specifies how many stack frames above lives the source line to be identified in the log message. - -Log lines have this form: - Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg... -where the fields are defined as follows: - L A single character, representing the log level (eg 'I' for INFO) - mm The month (zero padded; ie May is '05') - dd The day (zero padded) - hh:mm:ss.uuuuuu Time in hours, minutes and fractional seconds - threadid The space-padded thread ID as returned by GetTID() - file The file name - line The line number - msg The user-supplied message -*/ -func (l *loggingT) header(s severity, depth int) (*buffer, string, int) { - _, file, line, ok := runtime.Caller(3 + depth) +// ctxlogf writes a log message for a log function call (or log function wrapper) +// at the given depth in the current goroutine's stack. +func ctxlogf(ctx context.Context, depth int, severity logsink.Severity, verbose bool, stack stack, format string, args ...any) { + now := timeNow() + _, file, line, ok := runtime.Caller(depth + 1) if !ok { file = "???" line = 1 - } else { - slash := strings.LastIndex(file, "/") - if slash >= 0 { - file = file[slash+1:] - } } - return l.formatHeader(s, file, line), file, line + + if stack == withStack || backtraceAt(file, line) { + format, args = appendBacktrace(depth+1, format, args) + } + + metai, meta := metaPoolGet() + *meta = logsink.Meta{ + Context: ctx, + Time: now, + File: file, + Line: line, + Depth: depth + 1, + Severity: severity, + Verbose: verbose, + Thread: int64(pid), + } + sinkf(meta, format, args...) + // Clear pointer fields so they can be garbage collected early. + meta.Context = nil + meta.Stack = nil + metaPool.Put(metai) } -// formatHeader formats a log header using the provided file name and line number. -func (l *loggingT) formatHeader(s severity, file string, line int) *buffer { - now := timeNow() - if line < 0 { - line = 0 // not a real line number, but acceptable to someDigits - } - if s > fatalLog { - s = infoLog // for safety. - } - buf := l.getBuffer() - - // Avoid Fprintf, for speed. The format is so simple that we can do it quickly by hand. - // It's worth about 3X. Fprintf is hard. - _, month, day := now.Date() - hour, minute, second := now.Clock() - // Lmmdd hh:mm:ss.uuuuuu threadid file:line] - buf.tmp[0] = severityChar[s] - buf.twoDigits(1, int(month)) - buf.twoDigits(3, day) - buf.tmp[5] = ' ' - buf.twoDigits(6, hour) - buf.tmp[8] = ':' - buf.twoDigits(9, minute) - buf.tmp[11] = ':' - buf.twoDigits(12, second) - buf.tmp[14] = '.' - buf.nDigits(6, 15, now.Nanosecond()/1000, '0') - buf.tmp[21] = ' ' - buf.nDigits(7, 22, pid, ' ') // TODO: should be TID - buf.tmp[29] = ' ' - buf.Write(buf.tmp[:30]) - buf.WriteString(file) - buf.tmp[0] = ':' - n := buf.someDigits(1, line) - buf.tmp[n+1] = ']' - buf.tmp[n+2] = ' ' - buf.Write(buf.tmp[:n+3]) - return buf -} - -// Some custom tiny helper functions to print the log header efficiently. - -const digits = "0123456789" - -// twoDigits formats a zero-prefixed two-digit integer at buf.tmp[i]. -func (buf *buffer) twoDigits(i, d int) { - buf.tmp[i+1] = digits[d%10] - d /= 10 - buf.tmp[i] = digits[d%10] -} - -// nDigits formats an n-digit integer at buf.tmp[i], -// padding with pad on the left. -// It assumes d >= 0. -func (buf *buffer) nDigits(n, i, d int, pad byte) { - j := n - 1 - for ; j >= 0 && d > 0; j-- { - buf.tmp[i+j] = digits[d%10] - d /= 10 - } - for ; j >= 0; j-- { - buf.tmp[i+j] = pad - } -} - -// someDigits formats a zero-prefixed variable-width integer at buf.tmp[i]. -func (buf *buffer) someDigits(i, d int) int { - // Print into the top, then copy down. We know there's space for at least - // a 10-digit number. - j := len(buf.tmp) - for { - j-- - buf.tmp[j] = digits[d%10] - d /= 10 - if d == 0 { - break - } - } - return copy(buf.tmp[i:], buf.tmp[j:]) -} - -func (l *loggingT) println(s severity, args ...interface{}) { - buf, file, line := l.header(s, 0) - fmt.Fprintln(buf, args...) - l.output(s, buf, file, line, false) -} - -func (l *loggingT) print(s severity, args ...interface{}) { - l.printDepth(s, 1, args...) -} - -func (l *loggingT) printDepth(s severity, depth int, args ...interface{}) { - buf, file, line := l.header(s, depth) - fmt.Fprint(buf, args...) - if buf.Bytes()[buf.Len()-1] != '\n' { - buf.WriteByte('\n') - } - l.output(s, buf, file, line, false) -} - -func (l *loggingT) printf(s severity, format string, args ...interface{}) { - buf, file, line := l.header(s, 0) - fmt.Fprintf(buf, format, args...) - if buf.Bytes()[buf.Len()-1] != '\n' { - buf.WriteByte('\n') - } - l.output(s, buf, file, line, false) -} - -// printWithFileLine behaves like print but uses the provided file and line number. If -// alsoLogToStderr is true, the log message always appears on standard error; it -// will also appear in the log file unless --logtostderr is set. -func (l *loggingT) printWithFileLine(s severity, file string, line int, alsoToStderr bool, args ...interface{}) { - buf := l.formatHeader(s, file, line) - fmt.Fprint(buf, args...) - if buf.Bytes()[buf.Len()-1] != '\n' { - buf.WriteByte('\n') - } - l.output(s, buf, file, line, alsoToStderr) -} - -// output writes the data to the log files and releases the buffer. -func (l *loggingT) output(s severity, buf *buffer, file string, line int, alsoToStderr bool) { - l.mu.Lock() - if l.traceLocation.isSet() { - if l.traceLocation.match(file, line) { - buf.Write(stacks(false)) - } - } - data := buf.Bytes() - if !flag.Parsed() { - os.Stderr.Write([]byte("ERROR: logging before flag.Parse: ")) - os.Stderr.Write(data) - } else if l.toStderr { - os.Stderr.Write(data) - } else { - if alsoToStderr || l.alsoToStderr || s >= l.stderrThreshold.get() { - os.Stderr.Write(data) - } - if l.file[s] == nil { - if err := l.createFiles(s); err != nil { - os.Stderr.Write(data) // Make sure the message appears somewhere. - l.exit(err) - } - } - switch s { - case fatalLog: - l.file[fatalLog].Write(data) - fallthrough - case errorLog: - l.file[errorLog].Write(data) - fallthrough - case warningLog: - l.file[warningLog].Write(data) - fallthrough - case infoLog: - l.file[infoLog].Write(data) - } - } - if s == fatalLog { - // If we got here via Exit rather than Fatal, print no stacks. - if atomic.LoadUint32(&fatalNoStacks) > 0 { - l.mu.Unlock() - timeoutFlush(10 * time.Second) - os.Exit(1) - } - // Dump all goroutine stacks before exiting. - // First, make sure we see the trace for the current goroutine on standard error. - // If -logtostderr has been specified, the loop below will do that anyway - // as the first stack in the full dump. - if !l.toStderr { - os.Stderr.Write(stacks(false)) - } - // Write the stack trace for all goroutines to the files. - trace := stacks(true) - logExitFunc = func(error) {} // If we get a write error, we'll still exit below. - for log := fatalLog; log >= infoLog; log-- { - if f := l.file[log]; f != nil { // Can be nil if -logtostderr is set. - f.Write(trace) - } - } - l.mu.Unlock() - timeoutFlush(10 * time.Second) - os.Exit(255) // C++ uses -1, which is silly because it's anded with 255 anyway. - } - l.putBuffer(buf) - l.mu.Unlock() - if stats := severityStats[s]; stats != nil { +func sinkf(meta *logsink.Meta, format string, args ...any) { + meta.Depth++ + n, err := logsink.Printf(meta, format, args...) + if stats := severityStats[meta.Severity]; stats != nil { atomic.AddInt64(&stats.lines, 1) - atomic.AddInt64(&stats.bytes, int64(len(data))) + atomic.AddInt64(&stats.bytes, int64(n)) } -} -// timeoutFlush calls Flush and returns when it completes or after timeout -// elapses, whichever happens first. This is needed because the hooks invoked -// by Flush may deadlock when glog.Fatal is called from a hook that holds -// a lock. -func timeoutFlush(timeout time.Duration) { - done := make(chan bool, 1) - go func() { - Flush() // calls logging.lockAndFlushAll() - done <- true - }() - select { - case <-done: - case <-time.After(timeout): - fmt.Fprintln(os.Stderr, "glog: Flush took longer than", timeout) - } -} - -// stacks is a wrapper for runtime.Stack that attempts to recover the data for all goroutines. -func stacks(all bool) []byte { - // We don't know how big the traces are, so grow a few times if they don't fit. Start large, though. - n := 10000 - if all { - n = 100000 - } - var trace []byte - for i := 0; i < 5; i++ { - trace = make([]byte, n) - nbytes := runtime.Stack(trace, all) - if nbytes < len(trace) { - return trace[:nbytes] - } - n *= 2 - } - return trace -} - -// logExitFunc provides a simple mechanism to override the default behavior -// of exiting on error. Used in testing and to guarantee we reach a required exit -// for fatal logs. Instead, exit could be a function rather than a method but that -// would make its use clumsier. -var logExitFunc func(error) - -// exit is called if there is trouble creating or writing log files. -// It flushes the logs and exits the program; there's no point in hanging around. -// l.mu is held. -func (l *loggingT) exit(err error) { - fmt.Fprintf(os.Stderr, "log: exiting because of error: %s\n", err) - // If logExitFunc is set, we do that instead of exiting. - if logExitFunc != nil { - logExitFunc(err) - return - } - l.flushAll() - os.Exit(2) -} - -// syncBuffer joins a bufio.Writer to its underlying file, providing access to the -// file's Sync method and providing a wrapper for the Write method that provides log -// file rotation. There are conflicting methods, so the file cannot be embedded. -// l.mu is held for all its methods. -type syncBuffer struct { - logger *loggingT - *bufio.Writer - file *os.File - sev severity - nbytes uint64 // The number of bytes written to this file -} - -func (sb *syncBuffer) Sync() error { - return sb.file.Sync() -} - -func (sb *syncBuffer) Write(p []byte) (n int, err error) { - if sb.nbytes+uint64(len(p)) >= MaxSize { - if err := sb.rotateFile(time.Now()); err != nil { - sb.logger.exit(err) - } - } - n, err = sb.Writer.Write(p) - sb.nbytes += uint64(n) if err != nil { - sb.logger.exit(err) - } - return -} - -// rotateFile closes the syncBuffer's file and starts a new one. -func (sb *syncBuffer) rotateFile(now time.Time) error { - if sb.file != nil { - sb.Flush() - sb.file.Close() - } - var err error - sb.file, _, err = create(severityName[sb.sev], now) - sb.nbytes = 0 - if err != nil { - return err - } - - sb.Writer = bufio.NewWriterSize(sb.file, bufferSize) - - // Write header. - var buf bytes.Buffer - fmt.Fprintf(&buf, "Log file created at: %s\n", now.Format("2006/01/02 15:04:05")) - fmt.Fprintf(&buf, "Running on machine: %s\n", host) - fmt.Fprintf(&buf, "Binary: Built with %s %s for %s/%s\n", runtime.Compiler, runtime.Version(), runtime.GOOS, runtime.GOARCH) - fmt.Fprintf(&buf, "Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg\n") - n, err := sb.file.Write(buf.Bytes()) - sb.nbytes += uint64(n) - return err -} - -// bufferSize sizes the buffer associated with each log file. It's large -// so that log records can accumulate without the logging thread blocking -// on disk I/O. The flushDaemon will block instead. -const bufferSize = 256 * 1024 - -// createFiles creates all the log files for severity from sev down to infoLog. -// l.mu is held. -func (l *loggingT) createFiles(sev severity) error { - now := time.Now() - // Files are created in decreasing severity order, so as soon as we find one - // has already been created, we can stop. - for s := sev; s >= infoLog && l.file[s] == nil; s-- { - sb := &syncBuffer{ - logger: l, - sev: s, - } - if err := sb.rotateFile(now); err != nil { - return err - } - l.file[s] = sb - } - return nil -} - -const flushInterval = 30 * time.Second - -// flushDaemon periodically flushes the log file buffers. -func (l *loggingT) flushDaemon() { - for _ = range time.NewTicker(flushInterval).C { - l.lockAndFlushAll() - } -} - -// lockAndFlushAll is like flushAll but locks l.mu first. -func (l *loggingT) lockAndFlushAll() { - l.mu.Lock() - l.flushAll() - l.mu.Unlock() -} - -// flushAll flushes all the logs and attempts to "sync" their data to disk. -// l.mu is held. -func (l *loggingT) flushAll() { - // Flush from fatal down, in case there's trouble flushing. - for s := fatalLog; s >= infoLog; s-- { - file := l.file[s] - if file != nil { - file.Flush() // ignore error - file.Sync() // ignore error - } + logsink.Printf(meta, "glog: exiting because of error: %s", err) + sinks.file.Flush() + os.Exit(2) } } @@ -912,9 +261,9 @@ func (l *loggingT) flushAll() { // Valid names are "INFO", "WARNING", "ERROR", and "FATAL". If the name is not // recognized, CopyStandardLogTo panics. func CopyStandardLogTo(name string) { - sev, ok := severityByName(name) - if !ok { - panic(fmt.Sprintf("log.CopyStandardLogTo(%q): unrecognized severity name", name)) + sev, err := logsink.ParseSeverity(name) + if err != nil { + panic(fmt.Sprintf("log.CopyStandardLogTo(%q): %v", name, err)) } // Set a log format that captures the user's file and line: // d.go:23: message @@ -922,9 +271,22 @@ func CopyStandardLogTo(name string) { stdLog.SetOutput(logBridge(sev)) } +// NewStandardLogger returns a Logger that writes to the Google logs for the +// named and lower severities. +// +// Valid names are "INFO", "WARNING", "ERROR", and "FATAL". If the name is not +// recognized, NewStandardLogger panics. +func NewStandardLogger(name string) *stdLog.Logger { + sev, err := logsink.ParseSeverity(name) + if err != nil { + panic(fmt.Sprintf("log.NewStandardLogger(%q): %v", name, err)) + } + return stdLog.New(logBridge(sev), "", stdLog.Lshortfile) +} + // logBridge provides the Write method that enables CopyStandardLogTo to connect // Go's standard logs to the logs provided by this package. -type logBridge severity +type logBridge logsink.Severity // Write parses the standard logging line and passes its components to the // logger for severity(lb). @@ -946,36 +308,72 @@ func (lb logBridge) Write(b []byte) (n int, err error) { line = 1 } } - // printWithFileLine with alsoToStderr=true, so standard log messages - // always appear on standard error. - logging.printWithFileLine(severity(lb), file, line, true, text) + + // The depth below hard-codes details of how stdlog gets here. The alternative would be to walk + // up the stack looking for src/log/log.go but that seems like it would be + // unfortunately slow. + const stdLogDepth = 4 + + metai, meta := metaPoolGet() + *meta = logsink.Meta{ + Time: timeNow(), + File: file, + Line: line, + Depth: stdLogDepth, + Severity: logsink.Severity(lb), + Thread: int64(pid), + } + + format := "%s" + args := []any{text} + if backtraceAt(file, line) { + format, args = appendBacktrace(meta.Depth, format, args) + } + + sinkf(meta, format, args...) + metaPool.Put(metai) + return len(b), nil } -// setV computes and remembers the V level for a given PC -// when vmodule is enabled. -// File pattern matching takes the basename of the file, stripped -// of its .go suffix, and uses filepath.Match, which is a little more -// general than the *? matching used in C++. -// l.mu is held. -func (l *loggingT) setV(pc uintptr) Level { - fn := runtime.FuncForPC(pc) - file, _ := fn.FileLine(pc) - // The file is something like /a/b/c/d.go. We want just the d. - if strings.HasSuffix(file, ".go") { - file = file[:len(file)-3] +// defaultFormat returns a fmt.Printf format specifier that formats its +// arguments as if they were passed to fmt.Print. +func defaultFormat(args []any) string { + n := len(args) + switch n { + case 0: + return "" + case 1: + return "%v" } - if slash := strings.LastIndex(file, "/"); slash >= 0 { - file = file[slash+1:] - } - for _, filter := range l.vmodule.filter { - if filter.match(file) { - l.vmap[pc] = filter.level - return filter.level + + b := make([]byte, 0, n*3-1) + wasString := true // Suppress leading space. + for _, arg := range args { + isString := arg != nil && reflect.TypeOf(arg).Kind() == reflect.String + if wasString || isString { + b = append(b, "%v"...) + } else { + b = append(b, " %v"...) } + wasString = isString } - l.vmap[pc] = 0 - return 0 + return string(b) +} + +// lnFormat returns a fmt.Printf format specifier that formats its arguments +// as if they were passed to fmt.Println. +func lnFormat(args []any) string { + if len(args) == 0 { + return "\n" + } + + b := make([]byte, 0, len(args)*3) + for range args { + b = append(b, "%v "...) + } + b[len(b)-1] = '\n' // Replace the last space with a newline. + return string(b) } // Verbose is a boolean type that implements Infof (like Printf) etc. @@ -986,195 +384,394 @@ type Verbose bool // The returned value is a boolean of type Verbose, which implements Info, Infoln // and Infof. These methods will write to the Info log if called. // Thus, one may write either +// // if glog.V(2) { glog.Info("log this") } +// // or +// // glog.V(2).Info("log this") +// // The second form is shorter but the first is cheaper if logging is off because it does // not evaluate its arguments. // // Whether an individual call to V generates a log record depends on the setting of // the -v and --vmodule flags; both are off by default. If the level in the call to -// V is at least the value of -v, or of -vmodule for the source file containing the +// V is at most the value of -v, or of -vmodule for the source file containing the // call, the V call will log. func V(level Level) Verbose { - // This function tries hard to be cheap unless there's work to do. - // The fast path is two atomic loads and compares. + return VDepth(1, level) +} - // Here is a cheap but safe test to see if V logging is enabled globally. - if logging.verbosity.get() >= level { - return Verbose(true) - } - - // It's off globally but it vmodule may still be set. - // Here is another cheap but safe test to see if vmodule is enabled. - if atomic.LoadInt32(&logging.filterLength) > 0 { - // Now we need a proper lock to use the logging structure. The pcs field - // is shared so we must lock before accessing it. This is fairly expensive, - // but if V logging is enabled we're slow anyway. - logging.mu.Lock() - defer logging.mu.Unlock() - if runtime.Callers(2, logging.pcs[:]) == 0 { - return Verbose(false) - } - v, ok := logging.vmap[logging.pcs[0]] - if !ok { - v = logging.setV(logging.pcs[0]) - } - return Verbose(v >= level) - } - return Verbose(false) +// VDepth acts as V but uses depth to determine which call frame to check vmodule for. +// VDepth(0, level) is the same as V(level). +func VDepth(depth int, level Level) Verbose { + return Verbose(verboseEnabled(depth+1, level)) } // Info is equivalent to the global Info function, guarded by the value of v. // See the documentation of V for usage. -func (v Verbose) Info(args ...interface{}) { +func (v Verbose) Info(args ...any) { + v.InfoDepth(1, args...) +} + +// InfoDepth is equivalent to the global InfoDepth function, guarded by the value of v. +// See the documentation of V for usage. +func (v Verbose) InfoDepth(depth int, args ...any) { if v { - logging.print(infoLog, args...) + logf(depth+1, logsink.Info, true, noStack, defaultFormat(args), args...) + } +} + +// InfoDepthf is equivalent to the global InfoDepthf function, guarded by the value of v. +// See the documentation of V for usage. +func (v Verbose) InfoDepthf(depth int, format string, args ...any) { + if v { + logf(depth+1, logsink.Info, true, noStack, format, args...) } } // Infoln is equivalent to the global Infoln function, guarded by the value of v. // See the documentation of V for usage. -func (v Verbose) Infoln(args ...interface{}) { +func (v Verbose) Infoln(args ...any) { if v { - logging.println(infoLog, args...) + logf(1, logsink.Info, true, noStack, lnFormat(args), args...) } } // Infof is equivalent to the global Infof function, guarded by the value of v. // See the documentation of V for usage. -func (v Verbose) Infof(format string, args ...interface{}) { +func (v Verbose) Infof(format string, args ...any) { if v { - logging.printf(infoLog, format, args...) + logf(1, logsink.Info, true, noStack, format, args...) + } +} + +// InfoContext is equivalent to the global InfoContext function, guarded by the value of v. +// See the documentation of V for usage. +func (v Verbose) InfoContext(ctx context.Context, args ...any) { + v.InfoContextDepth(ctx, 1, args...) +} + +// InfoContextf is equivalent to the global InfoContextf function, guarded by the value of v. +// See the documentation of V for usage. +func (v Verbose) InfoContextf(ctx context.Context, format string, args ...any) { + if v { + ctxlogf(ctx, 1, logsink.Info, true, noStack, format, args...) + } +} + +// InfoContextDepth is equivalent to the global InfoContextDepth function, guarded by the value of v. +// See the documentation of V for usage. +func (v Verbose) InfoContextDepth(ctx context.Context, depth int, args ...any) { + if v { + ctxlogf(ctx, depth+1, logsink.Info, true, noStack, defaultFormat(args), args...) + } +} + +// InfoContextDepthf is equivalent to the global InfoContextDepthf function, guarded by the value of v. +// See the documentation of V for usage. +func (v Verbose) InfoContextDepthf(ctx context.Context, depth int, format string, args ...any) { + if v { + ctxlogf(ctx, depth+1, logsink.Info, true, noStack, format, args...) } } // Info logs to the INFO log. // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. -func Info(args ...interface{}) { - logging.print(infoLog, args...) +func Info(args ...any) { + InfoDepth(1, args...) } -// InfoDepth acts as Info but uses depth to determine which call frame to log. -// InfoDepth(0, "msg") is the same as Info("msg"). -func InfoDepth(depth int, args ...interface{}) { - logging.printDepth(infoLog, depth, args...) +// InfoDepth calls Info from a different depth in the call stack. +// This enables a callee to emit logs that use the callsite information of its caller +// or any other callers in the stack. When depth == 0, the original callee's line +// information is emitted. When depth > 0, depth frames are skipped in the call stack +// and the final frame is treated like the original callee to Info. +func InfoDepth(depth int, args ...any) { + logf(depth+1, logsink.Info, false, noStack, defaultFormat(args), args...) +} + +// InfoDepthf acts as InfoDepth but with format string. +func InfoDepthf(depth int, format string, args ...any) { + logf(depth+1, logsink.Info, false, noStack, format, args...) } // Infoln logs to the INFO log. // Arguments are handled in the manner of fmt.Println; a newline is appended if missing. -func Infoln(args ...interface{}) { - logging.println(infoLog, args...) +func Infoln(args ...any) { + logf(1, logsink.Info, false, noStack, lnFormat(args), args...) } // Infof logs to the INFO log. // Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. -func Infof(format string, args ...interface{}) { - logging.printf(infoLog, format, args...) +func Infof(format string, args ...any) { + logf(1, logsink.Info, false, noStack, format, args...) +} + +// InfoContext is like [Info], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func InfoContext(ctx context.Context, args ...any) { + InfoContextDepth(ctx, 1, args...) +} + +// InfoContextf is like [Infof], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func InfoContextf(ctx context.Context, format string, args ...any) { + ctxlogf(ctx, 1, logsink.Info, false, noStack, format, args...) +} + +// InfoContextDepth is like [InfoDepth], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func InfoContextDepth(ctx context.Context, depth int, args ...any) { + ctxlogf(ctx, depth+1, logsink.Info, false, noStack, defaultFormat(args), args...) +} + +// InfoContextDepthf is like [InfoDepthf], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func InfoContextDepthf(ctx context.Context, depth int, format string, args ...any) { + ctxlogf(ctx, depth+1, logsink.Info, false, noStack, format, args...) } // Warning logs to the WARNING and INFO logs. // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. -func Warning(args ...interface{}) { - logging.print(warningLog, args...) +func Warning(args ...any) { + WarningDepth(1, args...) } // WarningDepth acts as Warning but uses depth to determine which call frame to log. // WarningDepth(0, "msg") is the same as Warning("msg"). -func WarningDepth(depth int, args ...interface{}) { - logging.printDepth(warningLog, depth, args...) +func WarningDepth(depth int, args ...any) { + logf(depth+1, logsink.Warning, false, noStack, defaultFormat(args), args...) +} + +// WarningDepthf acts as Warningf but uses depth to determine which call frame to log. +// WarningDepthf(0, "msg") is the same as Warningf("msg"). +func WarningDepthf(depth int, format string, args ...any) { + logf(depth+1, logsink.Warning, false, noStack, format, args...) } // Warningln logs to the WARNING and INFO logs. // Arguments are handled in the manner of fmt.Println; a newline is appended if missing. -func Warningln(args ...interface{}) { - logging.println(warningLog, args...) +func Warningln(args ...any) { + logf(1, logsink.Warning, false, noStack, lnFormat(args), args...) } // Warningf logs to the WARNING and INFO logs. // Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. -func Warningf(format string, args ...interface{}) { - logging.printf(warningLog, format, args...) +func Warningf(format string, args ...any) { + logf(1, logsink.Warning, false, noStack, format, args...) +} + +// WarningContext is like [Warning], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func WarningContext(ctx context.Context, args ...any) { + WarningContextDepth(ctx, 1, args...) +} + +// WarningContextf is like [Warningf], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func WarningContextf(ctx context.Context, format string, args ...any) { + ctxlogf(ctx, 1, logsink.Warning, false, noStack, format, args...) +} + +// WarningContextDepth is like [WarningDepth], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func WarningContextDepth(ctx context.Context, depth int, args ...any) { + ctxlogf(ctx, depth+1, logsink.Warning, false, noStack, defaultFormat(args), args...) +} + +// WarningContextDepthf is like [WarningDepthf], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func WarningContextDepthf(ctx context.Context, depth int, format string, args ...any) { + ctxlogf(ctx, depth+1, logsink.Warning, false, noStack, format, args...) } // Error logs to the ERROR, WARNING, and INFO logs. // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. -func Error(args ...interface{}) { - logging.print(errorLog, args...) +func Error(args ...any) { + ErrorDepth(1, args...) } // ErrorDepth acts as Error but uses depth to determine which call frame to log. // ErrorDepth(0, "msg") is the same as Error("msg"). -func ErrorDepth(depth int, args ...interface{}) { - logging.printDepth(errorLog, depth, args...) +func ErrorDepth(depth int, args ...any) { + logf(depth+1, logsink.Error, false, noStack, defaultFormat(args), args...) +} + +// ErrorDepthf acts as Errorf but uses depth to determine which call frame to log. +// ErrorDepthf(0, "msg") is the same as Errorf("msg"). +func ErrorDepthf(depth int, format string, args ...any) { + logf(depth+1, logsink.Error, false, noStack, format, args...) } // Errorln logs to the ERROR, WARNING, and INFO logs. // Arguments are handled in the manner of fmt.Println; a newline is appended if missing. -func Errorln(args ...interface{}) { - logging.println(errorLog, args...) +func Errorln(args ...any) { + logf(1, logsink.Error, false, noStack, lnFormat(args), args...) } // Errorf logs to the ERROR, WARNING, and INFO logs. // Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. -func Errorf(format string, args ...interface{}) { - logging.printf(errorLog, format, args...) +func Errorf(format string, args ...any) { + logf(1, logsink.Error, false, noStack, format, args...) +} + +// ErrorContext is like [Error], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func ErrorContext(ctx context.Context, args ...any) { + ErrorContextDepth(ctx, 1, args...) +} + +// ErrorContextf is like [Errorf], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func ErrorContextf(ctx context.Context, format string, args ...any) { + ctxlogf(ctx, 1, logsink.Error, false, noStack, format, args...) +} + +// ErrorContextDepth is like [ErrorDepth], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func ErrorContextDepth(ctx context.Context, depth int, args ...any) { + ctxlogf(ctx, depth+1, logsink.Error, false, noStack, defaultFormat(args), args...) +} + +// ErrorContextDepthf is like [ErrorDepthf], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func ErrorContextDepthf(ctx context.Context, depth int, format string, args ...any) { + ctxlogf(ctx, depth+1, logsink.Error, false, noStack, format, args...) +} + +func ctxfatalf(ctx context.Context, depth int, format string, args ...any) { + ctxlogf(ctx, depth+1, logsink.Fatal, false, withStack, format, args...) + sinks.file.Flush() + + err := abortProcess() // Should not return. + + // Failed to abort the process using signals. Dump a stack trace and exit. + Errorf("abortProcess returned unexpectedly: %v", err) + sinks.file.Flush() + pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) + os.Exit(2) // Exit with the same code as the default SIGABRT handler. +} + +func fatalf(depth int, format string, args ...any) { + ctxfatalf(nil, depth+1, format, args...) } // Fatal logs to the FATAL, ERROR, WARNING, and INFO logs, -// including a stack trace of all running goroutines, then calls os.Exit(255). +// including a stack trace of all running goroutines, then calls os.Exit(2). // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. -func Fatal(args ...interface{}) { - logging.print(fatalLog, args...) +func Fatal(args ...any) { + FatalDepth(1, args...) } // FatalDepth acts as Fatal but uses depth to determine which call frame to log. // FatalDepth(0, "msg") is the same as Fatal("msg"). -func FatalDepth(depth int, args ...interface{}) { - logging.printDepth(fatalLog, depth, args...) +func FatalDepth(depth int, args ...any) { + fatalf(depth+1, defaultFormat(args), args...) +} + +// FatalDepthf acts as Fatalf but uses depth to determine which call frame to log. +// FatalDepthf(0, "msg") is the same as Fatalf("msg"). +func FatalDepthf(depth int, format string, args ...any) { + fatalf(depth+1, format, args...) } // Fatalln logs to the FATAL, ERROR, WARNING, and INFO logs, -// including a stack trace of all running goroutines, then calls os.Exit(255). +// including a stack trace of all running goroutines, then calls os.Exit(2). // Arguments are handled in the manner of fmt.Println; a newline is appended if missing. -func Fatalln(args ...interface{}) { - logging.println(fatalLog, args...) +func Fatalln(args ...any) { + fatalf(1, lnFormat(args), args...) } // Fatalf logs to the FATAL, ERROR, WARNING, and INFO logs, -// including a stack trace of all running goroutines, then calls os.Exit(255). +// including a stack trace of all running goroutines, then calls os.Exit(2). // Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. -func Fatalf(format string, args ...interface{}) { - logging.printf(fatalLog, format, args...) +func Fatalf(format string, args ...any) { + fatalf(1, format, args...) } -// fatalNoStacks is non-zero if we are to exit without dumping goroutine stacks. -// It allows Exit and relatives to use the Fatal logs. -var fatalNoStacks uint32 +// FatalContext is like [Fatal], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func FatalContext(ctx context.Context, args ...any) { + FatalContextDepth(ctx, 1, args...) +} + +// FatalContextf is like [Fatalf], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func FatalContextf(ctx context.Context, format string, args ...any) { + ctxfatalf(ctx, 1, format, args...) +} + +// FatalContextDepth is like [FatalDepth], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func FatalContextDepth(ctx context.Context, depth int, args ...any) { + ctxfatalf(ctx, depth+1, defaultFormat(args), args...) +} + +// FatalContextDepthf is like [FatalDepthf], but with an extra [context.Context] parameter. +func FatalContextDepthf(ctx context.Context, depth int, format string, args ...any) { + ctxfatalf(ctx, depth+1, format, args...) +} + +func ctxexitf(ctx context.Context, depth int, format string, args ...any) { + ctxlogf(ctx, depth+1, logsink.Fatal, false, noStack, format, args...) + sinks.file.Flush() + os.Exit(1) +} + +func exitf(depth int, format string, args ...any) { + ctxexitf(nil, depth+1, format, args...) +} // Exit logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1). // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. -func Exit(args ...interface{}) { - atomic.StoreUint32(&fatalNoStacks, 1) - logging.print(fatalLog, args...) +func Exit(args ...any) { + ExitDepth(1, args...) } // ExitDepth acts as Exit but uses depth to determine which call frame to log. // ExitDepth(0, "msg") is the same as Exit("msg"). -func ExitDepth(depth int, args ...interface{}) { - atomic.StoreUint32(&fatalNoStacks, 1) - logging.printDepth(fatalLog, depth, args...) +func ExitDepth(depth int, args ...any) { + exitf(depth+1, defaultFormat(args), args...) +} + +// ExitDepthf acts as Exitf but uses depth to determine which call frame to log. +// ExitDepthf(0, "msg") is the same as Exitf("msg"). +func ExitDepthf(depth int, format string, args ...any) { + exitf(depth+1, format, args...) } // Exitln logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1). -func Exitln(args ...interface{}) { - atomic.StoreUint32(&fatalNoStacks, 1) - logging.println(fatalLog, args...) +func Exitln(args ...any) { + exitf(1, lnFormat(args), args...) } // Exitf logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1). // Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. -func Exitf(format string, args ...interface{}) { - atomic.StoreUint32(&fatalNoStacks, 1) - logging.printf(fatalLog, format, args...) +func Exitf(format string, args ...any) { + exitf(1, format, args...) +} + +// ExitContext is like [Exit], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func ExitContext(ctx context.Context, args ...any) { + ExitContextDepth(ctx, 1, args...) +} + +// ExitContextf is like [Exitf], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func ExitContextf(ctx context.Context, format string, args ...any) { + ctxexitf(ctx, 1, format, args...) +} + +// ExitContextDepth is like [ExitDepth], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func ExitContextDepth(ctx context.Context, depth int, args ...any) { + ctxexitf(ctx, depth+1, defaultFormat(args), args...) +} + +// ExitContextDepthf is like [ExitDepthf], but with an extra [context.Context] parameter. The +// context is used to pass the Trace Context to log sinks. +func ExitContextDepthf(ctx context.Context, depth int, format string, args ...any) { + ctxexitf(ctx, depth+1, format, args...) } diff --git a/vendor/github.com/golang/glog/glog_file.go b/vendor/github.com/golang/glog/glog_file.go index 65075d28..e7d125c5 100644 --- a/vendor/github.com/golang/glog/glog_file.go +++ b/vendor/github.com/golang/glog/glog_file.go @@ -1,6 +1,6 @@ -// Go support for leveled logs, analogous to https://code.google.com/p/google-glog/ +// Go support for leveled logs, analogous to https://github.com/google/glog. // -// Copyright 2013 Google Inc. All Rights Reserved. +// Copyright 2023 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,26 +19,34 @@ package glog import ( + "bufio" + "bytes" "errors" "flag" "fmt" + "io" "os" "os/user" "path/filepath" + "runtime" "strings" "sync" "time" -) -// MaxSize is the maximum size of a log file in bytes. -var MaxSize uint64 = 1024 * 1024 * 1800 + "github.com/golang/glog/internal/logsink" +) // logDirs lists the candidate directories for new log files. var logDirs []string -// If non-empty, overrides the choice of directory in which to write logs. -// See createLogDirs for the full list of possible destinations. -var logDir = flag.String("log_dir", "", "If non-empty, write log files in this directory") +var ( + // If non-empty, overrides the choice of directory in which to write logs. + // See createLogDirs for the full list of possible destinations. + logDir = flag.String("log_dir", "", "If non-empty, write log files in this directory") + logLink = flag.String("log_link", "", "If non-empty, add symbolic links in this directory to the log files") + logBufLevel = flag.Int("logbuflevel", int(logsink.Info), "Buffer log messages logged at this level or lower"+ + " (-1 means don't buffer; 0 means buffer INFO only; ...). Has limited applicability on non-prod platforms.") +) func createLogDirs() { if *logDir != "" { @@ -64,9 +72,17 @@ func init() { if err == nil { userName = current.Username } - - // Sanitize userName since it may contain filepath separators on Windows. - userName = strings.Replace(userName, `\`, "_", -1) + // Sanitize userName since it is used to construct file paths. + userName = strings.Map(func(r rune) rune { + switch { + case r >= 'a' && r <= 'z': + case r >= 'A' && r <= 'Z': + case r >= '0' && r <= '9': + default: + return '_' + } + return r + }, userName) } // shortHostname returns its argument, truncating at the first period. @@ -116,9 +132,282 @@ func create(tag string, t time.Time) (f *os.File, filename string, err error) { symlink := filepath.Join(dir, link) os.Remove(symlink) // ignore err os.Symlink(name, symlink) // ignore err + if *logLink != "" { + lsymlink := filepath.Join(*logLink, link) + os.Remove(lsymlink) // ignore err + os.Symlink(fname, lsymlink) // ignore err + } return f, fname, nil } lastErr = err } return nil, "", fmt.Errorf("log: cannot create log: %v", lastErr) } + +// flushSyncWriter is the interface satisfied by logging destinations. +type flushSyncWriter interface { + Flush() error + Sync() error + io.Writer + filenames() []string +} + +var sinks struct { + stderr stderrSink + file fileSink +} + +func init() { + // Register stderr first: that way if we crash during file-writing at least + // the log will have gone somewhere. + logsink.TextSinks = append(logsink.TextSinks, &sinks.stderr, &sinks.file) + + sinks.file.flushChan = make(chan logsink.Severity, 1) + go sinks.file.flushDaemon() +} + +// stderrSink is a logsink.Text that writes log entries to stderr +// if they meet certain conditions. +type stderrSink struct { + mu sync.Mutex + w io.Writer // if nil Emit uses os.Stderr directly +} + +// Enabled implements logsink.Text.Enabled. It returns true if any of the +// various stderr flags are enabled for logs of the given severity, if the log +// message is from the standard "log" package, or if google.Init has not yet run +// (and hence file logging is not yet initialized). +func (s *stderrSink) Enabled(m *logsink.Meta) bool { + return toStderr || alsoToStderr || m.Severity >= stderrThreshold.get() +} + +// Emit implements logsink.Text.Emit. +func (s *stderrSink) Emit(m *logsink.Meta, data []byte) (n int, err error) { + s.mu.Lock() + defer s.mu.Unlock() + w := s.w + if w == nil { + w = os.Stderr + } + dn, err := w.Write(data) + n += dn + return n, err +} + +// severityWriters is an array of flushSyncWriter with a value for each +// logsink.Severity. +type severityWriters [4]flushSyncWriter + +// fileSink is a logsink.Text that prints to a set of Google log files. +type fileSink struct { + mu sync.Mutex + // file holds writer for each of the log types. + file severityWriters + flushChan chan logsink.Severity +} + +// Enabled implements logsink.Text.Enabled. It returns true if google.Init +// has run and both --disable_log_to_disk and --logtostderr are false. +func (s *fileSink) Enabled(m *logsink.Meta) bool { + return !toStderr +} + +// Emit implements logsink.Text.Emit +func (s *fileSink) Emit(m *logsink.Meta, data []byte) (n int, err error) { + s.mu.Lock() + defer s.mu.Unlock() + + if err = s.createMissingFiles(m.Severity); err != nil { + return 0, err + } + for sev := m.Severity; sev >= logsink.Info; sev-- { + if _, fErr := s.file[sev].Write(data); fErr != nil && err == nil { + err = fErr // Take the first error. + } + } + n = len(data) + if int(m.Severity) > *logBufLevel { + select { + case s.flushChan <- m.Severity: + default: + } + } + + return n, err +} + +// syncBuffer joins a bufio.Writer to its underlying file, providing access to the +// file's Sync method and providing a wrapper for the Write method that provides log +// file rotation. There are conflicting methods, so the file cannot be embedded. +// s.mu is held for all its methods. +type syncBuffer struct { + sink *fileSink + *bufio.Writer + file *os.File + names []string + sev logsink.Severity + nbytes uint64 // The number of bytes written to this file +} + +func (sb *syncBuffer) Sync() error { + return sb.file.Sync() +} + +func (sb *syncBuffer) Write(p []byte) (n int, err error) { + if sb.nbytes+uint64(len(p)) >= MaxSize { + if err := sb.rotateFile(time.Now()); err != nil { + return 0, err + } + } + n, err = sb.Writer.Write(p) + sb.nbytes += uint64(n) + return n, err +} + +func (sb *syncBuffer) filenames() []string { + return sb.names +} + +const footer = "\nCONTINUED IN NEXT FILE\n" + +// rotateFile closes the syncBuffer's file and starts a new one. +func (sb *syncBuffer) rotateFile(now time.Time) error { + var err error + pn := "" + file, name, err := create(sb.sev.String(), now) + + if sb.file != nil { + // The current log file becomes the previous log at the end of + // this block, so save its name for use in the header of the next + // file. + pn = sb.file.Name() + sb.Flush() + // If there's an existing file, write a footer with the name of + // the next file in the chain, followed by the constant string + // \nCONTINUED IN NEXT FILE\n to make continuation detection simple. + sb.file.Write([]byte("Next log: ")) + sb.file.Write([]byte(name)) + sb.file.Write([]byte(footer)) + sb.file.Close() + } + + sb.file = file + sb.names = append(sb.names, name) + sb.nbytes = 0 + if err != nil { + return err + } + + sb.Writer = bufio.NewWriterSize(sb.file, bufferSize) + + // Write header. + var buf bytes.Buffer + fmt.Fprintf(&buf, "Log file created at: %s\n", now.Format("2006/01/02 15:04:05")) + fmt.Fprintf(&buf, "Running on machine: %s\n", host) + fmt.Fprintf(&buf, "Binary: Built with %s %s for %s/%s\n", runtime.Compiler, runtime.Version(), runtime.GOOS, runtime.GOARCH) + fmt.Fprintf(&buf, "Previous log: %s\n", pn) + fmt.Fprintf(&buf, "Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg\n") + n, err := sb.file.Write(buf.Bytes()) + sb.nbytes += uint64(n) + return err +} + +// bufferSize sizes the buffer associated with each log file. It's large +// so that log records can accumulate without the logging thread blocking +// on disk I/O. The flushDaemon will block instead. +const bufferSize = 256 * 1024 + +// createMissingFiles creates all the log files for severity from infoLog up to +// upTo that have not already been created. +// s.mu is held. +func (s *fileSink) createMissingFiles(upTo logsink.Severity) error { + if s.file[upTo] != nil { + return nil + } + now := time.Now() + // Files are created in increasing severity order, so we can be assured that + // if a high severity logfile exists, then so do all of lower severity. + for sev := logsink.Info; sev <= upTo; sev++ { + if s.file[sev] != nil { + continue + } + sb := &syncBuffer{ + sink: s, + sev: sev, + } + if err := sb.rotateFile(now); err != nil { + return err + } + s.file[sev] = sb + } + return nil +} + +// flushDaemon periodically flushes the log file buffers. +func (s *fileSink) flushDaemon() { + tick := time.NewTicker(30 * time.Second) + defer tick.Stop() + for { + select { + case <-tick.C: + s.Flush() + case sev := <-s.flushChan: + s.flush(sev) + } + } +} + +// Flush flushes all pending log I/O. +func Flush() { + sinks.file.Flush() +} + +// Flush flushes all the logs and attempts to "sync" their data to disk. +func (s *fileSink) Flush() error { + return s.flush(logsink.Info) +} + +// flush flushes all logs of severity threshold or greater. +func (s *fileSink) flush(threshold logsink.Severity) error { + s.mu.Lock() + defer s.mu.Unlock() + + var firstErr error + updateErr := func(err error) { + if err != nil && firstErr == nil { + firstErr = err + } + } + + // Flush from fatal down, in case there's trouble flushing. + for sev := logsink.Fatal; sev >= threshold; sev-- { + file := s.file[sev] + if file != nil { + updateErr(file.Flush()) + updateErr(file.Sync()) + } + } + + return firstErr +} + +// Names returns the names of the log files holding the FATAL, ERROR, +// WARNING, or INFO logs. Returns ErrNoLog if the log for the given +// level doesn't exist (e.g. because no messages of that level have been +// written). This may return multiple names if the log type requested +// has rolled over. +func Names(s string) ([]string, error) { + severity, err := logsink.ParseSeverity(s) + if err != nil { + return nil, err + } + + sinks.file.mu.Lock() + defer sinks.file.mu.Unlock() + f := sinks.file.file[severity] + if f == nil { + return nil, ErrNoLog + } + + return f.filenames(), nil +} diff --git a/vendor/github.com/golang/glog/glog_file_linux.go b/vendor/github.com/golang/glog/glog_file_linux.go new file mode 100644 index 00000000..d795092d --- /dev/null +++ b/vendor/github.com/golang/glog/glog_file_linux.go @@ -0,0 +1,39 @@ +// Go support for leveled logs, analogous to https://github.com/google/glog. +// +// Copyright 2023 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux + +package glog + +import ( + "errors" + "runtime" + "syscall" +) + +// abortProcess attempts to kill the current process in a way that will dump the +// currently-running goroutines someplace useful (like stderr). +// +// It does this by sending SIGABRT to the current thread. +// +// If successful, abortProcess does not return. +func abortProcess() error { + runtime.LockOSThread() + if err := syscall.Tgkill(syscall.Getpid(), syscall.Gettid(), syscall.SIGABRT); err != nil { + return err + } + return errors.New("log: killed current thread with SIGABRT, but still running") +} diff --git a/vendor/github.com/golang/glog/glog_file_other.go b/vendor/github.com/golang/glog/glog_file_other.go new file mode 100644 index 00000000..9540f14f --- /dev/null +++ b/vendor/github.com/golang/glog/glog_file_other.go @@ -0,0 +1,30 @@ +// Go support for leveled logs, analogous to https://github.com/google/glog. +// +// Copyright 2023 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !(unix || windows) + +package glog + +import ( + "fmt" + "runtime" +) + +// abortProcess returns an error on platforms that presumably don't support signals. +func abortProcess() error { + return fmt.Errorf("not sending SIGABRT (%s/%s does not support signals), falling back", runtime.GOOS, runtime.GOARCH) + +} diff --git a/vendor/github.com/golang/glog/glog_file_posix.go b/vendor/github.com/golang/glog/glog_file_posix.go new file mode 100644 index 00000000..c27c7c0e --- /dev/null +++ b/vendor/github.com/golang/glog/glog_file_posix.go @@ -0,0 +1,53 @@ +// Go support for leveled logs, analogous to https://github.com/google/glog. +// +// Copyright 2023 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build (unix || windows) && !linux + +package glog + +import ( + "os" + "syscall" + "time" +) + +// abortProcess attempts to kill the current process in a way that will dump the +// currently-running goroutines someplace useful (like stderr). +// +// It does this by sending SIGABRT to the current process. Unfortunately, the +// signal may or may not be delivered to the current thread; in order to do that +// portably, we would need to add a cgo dependency and call pthread_kill. +// +// If successful, abortProcess does not return. +func abortProcess() error { + p, err := os.FindProcess(os.Getpid()) + if err != nil { + return err + } + if err := p.Signal(syscall.SIGABRT); err != nil { + return err + } + + // Sent the signal. Now we wait for it to arrive and any SIGABRT handlers to + // run (and eventually terminate the process themselves). + // + // We could just "select{}" here, but there's an outside chance that would + // trigger the runtime's deadlock detector if there happen not to be any + // background goroutines running. So we'll sleep a while first to give + // the signal some time. + time.Sleep(10 * time.Second) + select {} +} diff --git a/vendor/github.com/golang/glog/glog_flags.go b/vendor/github.com/golang/glog/glog_flags.go new file mode 100644 index 00000000..fa4371af --- /dev/null +++ b/vendor/github.com/golang/glog/glog_flags.go @@ -0,0 +1,398 @@ +// Go support for leveled logs, analogous to https://github.com/google/glog. +// +// Copyright 2023 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package glog + +import ( + "bytes" + "errors" + "flag" + "fmt" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" + "sync/atomic" + + "github.com/golang/glog/internal/logsink" +) + +// modulePat contains a filter for the -vmodule flag. +// It holds a verbosity level and a file pattern to match. +type modulePat struct { + pattern string + literal bool // The pattern is a literal string + full bool // The pattern wants to match the full path + level Level +} + +// match reports whether the file matches the pattern. It uses a string +// comparison if the pattern contains no metacharacters. +func (m *modulePat) match(full, file string) bool { + if m.literal { + if m.full { + return full == m.pattern + } + return file == m.pattern + } + if m.full { + match, _ := filepath.Match(m.pattern, full) + return match + } + match, _ := filepath.Match(m.pattern, file) + return match +} + +// isLiteral reports whether the pattern is a literal string, that is, has no metacharacters +// that require filepath.Match to be called to match the pattern. +func isLiteral(pattern string) bool { + return !strings.ContainsAny(pattern, `\*?[]`) +} + +// isFull reports whether the pattern matches the full file path, that is, +// whether it contains /. +func isFull(pattern string) bool { + return strings.ContainsRune(pattern, '/') +} + +// verboseFlags represents the setting of the -v and -vmodule flags. +type verboseFlags struct { + // moduleLevelCache is a sync.Map storing the -vmodule Level for each V() + // call site, identified by PC. If there is no matching -vmodule filter, + // the cached value is exactly v. moduleLevelCache is replaced with a new + // Map whenever the -vmodule or -v flag changes state. + moduleLevelCache atomic.Value + + // mu guards all fields below. + mu sync.Mutex + + // v stores the value of the -v flag. It may be read safely using + // sync.LoadInt32, but is only modified under mu. + v Level + + // module stores the parsed -vmodule flag. + module []modulePat + + // moduleLength caches len(module). If greater than zero, it + // means vmodule is enabled. It may be read safely using sync.LoadInt32, but + // is only modified under mu. + moduleLength int32 +} + +// NOTE: For compatibility with the open-sourced v1 version of this +// package (github.com/golang/glog) we need to retain that flag.Level +// implements the flag.Value interface. See also go/log-vs-glog. + +// String is part of the flag.Value interface. +func (l *Level) String() string { + return strconv.FormatInt(int64(l.Get().(Level)), 10) +} + +// Get is part of the flag.Value interface. +func (l *Level) Get() any { + if l == &vflags.v { + // l is the value registered for the -v flag. + return Level(atomic.LoadInt32((*int32)(l))) + } + return *l +} + +// Set is part of the flag.Value interface. +func (l *Level) Set(value string) error { + v, err := strconv.Atoi(value) + if err != nil { + return err + } + if l == &vflags.v { + // l is the value registered for the -v flag. + vflags.mu.Lock() + defer vflags.mu.Unlock() + vflags.moduleLevelCache.Store(&sync.Map{}) + atomic.StoreInt32((*int32)(l), int32(v)) + return nil + } + *l = Level(v) + return nil +} + +// vModuleFlag is the flag.Value for the --vmodule flag. +type vModuleFlag struct{ *verboseFlags } + +func (f vModuleFlag) String() string { + // Do not panic on the zero value. + // https://groups.google.com/g/golang-nuts/c/Atlr8uAjn6U/m/iId17Td5BQAJ. + if f.verboseFlags == nil { + return "" + } + f.mu.Lock() + defer f.mu.Unlock() + + var b bytes.Buffer + for i, f := range f.module { + if i > 0 { + b.WriteRune(',') + } + fmt.Fprintf(&b, "%s=%d", f.pattern, f.level) + } + return b.String() +} + +// Get returns nil for this flag type since the struct is not exported. +func (f vModuleFlag) Get() any { return nil } + +var errVmoduleSyntax = errors.New("syntax error: expect comma-separated list of filename=N") + +// Syntax: -vmodule=recordio=2,foo/bar/baz=1,gfs*=3 +func (f vModuleFlag) Set(value string) error { + var filter []modulePat + for _, pat := range strings.Split(value, ",") { + if len(pat) == 0 { + // Empty strings such as from a trailing comma can be ignored. + continue + } + patLev := strings.Split(pat, "=") + if len(patLev) != 2 || len(patLev[0]) == 0 || len(patLev[1]) == 0 { + return errVmoduleSyntax + } + pattern := patLev[0] + v, err := strconv.Atoi(patLev[1]) + if err != nil { + return errors.New("syntax error: expect comma-separated list of filename=N") + } + // TODO: check syntax of filter? + filter = append(filter, modulePat{pattern, isLiteral(pattern), isFull(pattern), Level(v)}) + } + + f.mu.Lock() + defer f.mu.Unlock() + f.module = filter + atomic.StoreInt32((*int32)(&f.moduleLength), int32(len(f.module))) + f.moduleLevelCache.Store(&sync.Map{}) + return nil +} + +func (f *verboseFlags) levelForPC(pc uintptr) Level { + if level, ok := f.moduleLevelCache.Load().(*sync.Map).Load(pc); ok { + return level.(Level) + } + + f.mu.Lock() + defer f.mu.Unlock() + level := Level(f.v) + fn := runtime.FuncForPC(pc) + file, _ := fn.FileLine(pc) + // The file is something like /a/b/c/d.go. We want just the d for + // regular matches, /a/b/c/d for full matches. + file = strings.TrimSuffix(file, ".go") + full := file + if slash := strings.LastIndex(file, "/"); slash >= 0 { + file = file[slash+1:] + } + for _, filter := range f.module { + if filter.match(full, file) { + level = filter.level + break // Use the first matching level. + } + } + f.moduleLevelCache.Load().(*sync.Map).Store(pc, level) + return level +} + +func (f *verboseFlags) enabled(callerDepth int, level Level) bool { + if atomic.LoadInt32(&f.moduleLength) == 0 { + // No vmodule values specified, so compare against v level. + return Level(atomic.LoadInt32((*int32)(&f.v))) >= level + } + + pcs := [1]uintptr{} + if runtime.Callers(callerDepth+2, pcs[:]) < 1 { + return false + } + frame, _ := runtime.CallersFrames(pcs[:]).Next() + return f.levelForPC(frame.Entry) >= level +} + +// traceLocation represents an entry in the -log_backtrace_at flag. +type traceLocation struct { + file string + line int +} + +var errTraceSyntax = errors.New("syntax error: expect file.go:234") + +func parseTraceLocation(value string) (traceLocation, error) { + fields := strings.Split(value, ":") + if len(fields) != 2 { + return traceLocation{}, errTraceSyntax + } + file, lineStr := fields[0], fields[1] + if !strings.Contains(file, ".") { + return traceLocation{}, errTraceSyntax + } + line, err := strconv.Atoi(lineStr) + if err != nil { + return traceLocation{}, errTraceSyntax + } + if line < 0 { + return traceLocation{}, errors.New("negative value for line") + } + return traceLocation{file, line}, nil +} + +// match reports whether the specified file and line matches the trace location. +// The argument file name is the full path, not the basename specified in the flag. +func (t traceLocation) match(file string, line int) bool { + if t.line != line { + return false + } + if i := strings.LastIndex(file, "/"); i >= 0 { + file = file[i+1:] + } + return t.file == file +} + +func (t traceLocation) String() string { + return fmt.Sprintf("%s:%d", t.file, t.line) +} + +// traceLocations represents the -log_backtrace_at flag. +// Syntax: -log_backtrace_at=recordio.go:234,sstable.go:456 +// Note that unlike vmodule the file extension is included here. +type traceLocations struct { + mu sync.Mutex + locsLen int32 // Safe for atomic read without mu. + locs []traceLocation +} + +func (t *traceLocations) String() string { + t.mu.Lock() + defer t.mu.Unlock() + + var buf bytes.Buffer + for i, tl := range t.locs { + if i > 0 { + buf.WriteString(",") + } + buf.WriteString(tl.String()) + } + return buf.String() +} + +// Get always returns nil for this flag type since the struct is not exported +func (t *traceLocations) Get() any { return nil } + +func (t *traceLocations) Set(value string) error { + var locs []traceLocation + for _, s := range strings.Split(value, ",") { + if s == "" { + continue + } + loc, err := parseTraceLocation(s) + if err != nil { + return err + } + locs = append(locs, loc) + } + + t.mu.Lock() + defer t.mu.Unlock() + atomic.StoreInt32(&t.locsLen, int32(len(locs))) + t.locs = locs + return nil +} + +func (t *traceLocations) match(file string, line int) bool { + if atomic.LoadInt32(&t.locsLen) == 0 { + return false + } + + t.mu.Lock() + defer t.mu.Unlock() + for _, tl := range t.locs { + if tl.match(file, line) { + return true + } + } + return false +} + +// severityFlag is an atomic flag.Value implementation for logsink.Severity. +type severityFlag int32 + +func (s *severityFlag) get() logsink.Severity { + return logsink.Severity(atomic.LoadInt32((*int32)(s))) +} +func (s *severityFlag) String() string { return strconv.FormatInt(int64(*s), 10) } +func (s *severityFlag) Get() any { return s.get() } +func (s *severityFlag) Set(value string) error { + threshold, err := logsink.ParseSeverity(value) + if err != nil { + // Not a severity name. Try a raw number. + v, err := strconv.Atoi(value) + if err != nil { + return err + } + threshold = logsink.Severity(v) + if threshold < logsink.Info || threshold > logsink.Fatal { + return fmt.Errorf("Severity %d out of range (min %d, max %d).", v, logsink.Info, logsink.Fatal) + } + } + atomic.StoreInt32((*int32)(s), int32(threshold)) + return nil +} + +var ( + vflags verboseFlags // The -v and -vmodule flags. + + logBacktraceAt traceLocations // The -log_backtrace_at flag. + + // Boolean flags. Not handled atomically because the flag.Value interface + // does not let us avoid the =true, and that shorthand is necessary for + // compatibility. TODO: does this matter enough to fix? Seems unlikely. + toStderr bool // The -logtostderr flag. + alsoToStderr bool // The -alsologtostderr flag. + + stderrThreshold severityFlag // The -stderrthreshold flag. +) + +// verboseEnabled returns whether the caller at the given depth should emit +// verbose logs at the given level, with depth 0 identifying the caller of +// verboseEnabled. +func verboseEnabled(callerDepth int, level Level) bool { + return vflags.enabled(callerDepth+1, level) +} + +// backtraceAt returns whether the logging call at the given function and line +// should also emit a backtrace of the current call stack. +func backtraceAt(file string, line int) bool { + return logBacktraceAt.match(file, line) +} + +func init() { + vflags.moduleLevelCache.Store(&sync.Map{}) + + flag.Var(&vflags.v, "v", "log level for V logs") + flag.Var(vModuleFlag{&vflags}, "vmodule", "comma-separated list of pattern=N settings for file-filtered logging") + + flag.Var(&logBacktraceAt, "log_backtrace_at", "when logging hits line file:N, emit a stack trace") + + stderrThreshold = severityFlag(logsink.Error) + + flag.BoolVar(&toStderr, "logtostderr", false, "log to standard error instead of files") + flag.BoolVar(&alsoToStderr, "alsologtostderr", false, "log to standard error as well as files") + flag.Var(&stderrThreshold, "stderrthreshold", "logs at or above this threshold go to stderr") +} diff --git a/vendor/github.com/golang/glog/internal/logsink/logsink.go b/vendor/github.com/golang/glog/internal/logsink/logsink.go new file mode 100644 index 00000000..28c38a6a --- /dev/null +++ b/vendor/github.com/golang/glog/internal/logsink/logsink.go @@ -0,0 +1,393 @@ +// Copyright 2023 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logsink + +import ( + "bytes" + "context" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/golang/glog/internal/stackdump" +) + +// MaxLogMessageLen is the limit on length of a formatted log message, including +// the standard line prefix and trailing newline. +// +// Chosen to match C++ glog. +const MaxLogMessageLen = 15000 + +// A Severity is a severity at which a message can be logged. +type Severity int8 + +// These constants identify the log levels in order of increasing severity. +// A message written to a high-severity log file is also written to each +// lower-severity log file. +const ( + Info Severity = iota + Warning + Error + + // Fatal contains logs written immediately before the process terminates. + // + // Sink implementations should not terminate the process themselves: the log + // package will perform any necessary cleanup and terminate the process as + // appropriate. + Fatal +) + +func (s Severity) String() string { + switch s { + case Info: + return "INFO" + case Warning: + return "WARNING" + case Error: + return "ERROR" + case Fatal: + return "FATAL" + } + return fmt.Sprintf("%T(%d)", s, s) +} + +// ParseSeverity returns the case-insensitive Severity value for the given string. +func ParseSeverity(name string) (Severity, error) { + name = strings.ToUpper(name) + for s := Info; s <= Fatal; s++ { + if s.String() == name { + return s, nil + } + } + return -1, fmt.Errorf("logsink: invalid severity %q", name) +} + +// Meta is metadata about a logging call. +type Meta struct { + // The context with which the log call was made (or nil). If set, the context + // is only valid during the logsink.Structured.Printf call, it should not be + // retained. + Context context.Context + + // Time is the time at which the log call was made. + Time time.Time + + // File is the source file from which the log entry originates. + File string + // Line is the line offset within the source file. + Line int + // Depth is the number of stack frames between the logsink and the log call. + Depth int + + Severity Severity + + // Verbose indicates whether the call was made via "log.V". Log entries below + // the current verbosity threshold are not sent to the sink. + Verbose bool + + // Thread ID. This can be populated with a thread ID from another source, + // such as a system we are importing logs from. In the normal case, this + // will be set to the process ID (PID), since Go doesn't have threads. + Thread int64 + + // Stack trace starting in the logging function. May be nil. + // A logsink should implement the StackWanter interface to request this. + // + // Even if WantStack returns false, this field may be set (e.g. if another + // sink wants a stack trace). + Stack *stackdump.Stack +} + +// Structured is a logging destination that accepts structured data as input. +type Structured interface { + // Printf formats according to a fmt.Printf format specifier and writes a log + // entry. The precise result of formatting depends on the sink, but should + // aim for consistency with fmt.Printf. + // + // Printf returns the number of bytes occupied by the log entry, which + // may not be equal to the total number of bytes written. + // + // Printf returns any error encountered *if* it is severe enough that the log + // package should terminate the process. + // + // The sink must not modify the *Meta parameter, nor reference it after + // Printf has returned: it may be reused in subsequent calls. + Printf(meta *Meta, format string, a ...any) (n int, err error) +} + +// StackWanter can be implemented by a logsink.Structured to indicate that it +// wants a stack trace to accompany at least some of the log messages it receives. +type StackWanter interface { + // WantStack returns true if the sink requires a stack trace for a log message + // with this metadata. + // + // NOTE: Returning true implies that meta.Stack will be non-nil. Returning + // false does NOT imply that meta.Stack will be nil. + WantStack(meta *Meta) bool +} + +// Text is a logging destination that accepts pre-formatted log lines (instead of +// structured data). +type Text interface { + // Enabled returns whether this sink should output messages for the given + // Meta. If the sink returns false for a given Meta, the Printf function will + // not call Emit on it for the corresponding log message. + Enabled(*Meta) bool + + // Emit writes a pre-formatted text log entry (including any applicable + // header) to the log. It returns the number of bytes occupied by the entry + // (which may differ from the length of the passed-in slice). + // + // Emit returns any error encountered *if* it is severe enough that the log + // package should terminate the process. + // + // The sink must not modify the *Meta parameter, nor reference it after + // Printf has returned: it may be reused in subsequent calls. + // + // NOTE: When developing a text sink, keep in mind the surface in which the + // logs will be displayed, and whether it's important that the sink be + // resistent to tampering in the style of b/211428300. Standard text sinks + // (like `stderrSink`) do not protect against this (e.g. by escaping + // characters) because the cases where they would show user-influenced bytes + // are vanishingly small. + Emit(*Meta, []byte) (n int, err error) +} + +// bufs is a pool of *bytes.Buffer used in formatting log entries. +var bufs sync.Pool // Pool of *bytes.Buffer. + +// textPrintf formats a text log entry and emits it to all specified Text sinks. +// +// The returned n is the maximum across all Emit calls. +// The returned err is the first non-nil error encountered. +// Sinks that are disabled by configuration should return (0, nil). +func textPrintf(m *Meta, textSinks []Text, format string, args ...any) (n int, err error) { + // We expect at most file, stderr, and perhaps syslog. If there are more, + // we'll end up allocating - no big deal. + const maxExpectedTextSinks = 3 + var noAllocSinks [maxExpectedTextSinks]Text + + sinks := noAllocSinks[:0] + for _, s := range textSinks { + if s.Enabled(m) { + sinks = append(sinks, s) + } + } + if len(sinks) == 0 && m.Severity != Fatal { + return 0, nil // No TextSinks specified; don't bother formatting. + } + + bufi := bufs.Get() + var buf *bytes.Buffer + if bufi == nil { + buf = bytes.NewBuffer(nil) + bufi = buf + } else { + buf = bufi.(*bytes.Buffer) + buf.Reset() + } + + // Lmmdd hh:mm:ss.uuuuuu PID/GID file:line] + // + // The "PID" entry arguably ought to be TID for consistency with other + // environments, but TID is not meaningful in a Go program due to the + // multiplexing of goroutines across threads. + // + // Avoid Fprintf, for speed. The format is so simple that we can do it quickly by hand. + // It's worth about 3X. Fprintf is hard. + const severityChar = "IWEF" + buf.WriteByte(severityChar[m.Severity]) + + _, month, day := m.Time.Date() + hour, minute, second := m.Time.Clock() + twoDigits(buf, int(month)) + twoDigits(buf, day) + buf.WriteByte(' ') + twoDigits(buf, hour) + buf.WriteByte(':') + twoDigits(buf, minute) + buf.WriteByte(':') + twoDigits(buf, second) + buf.WriteByte('.') + nDigits(buf, 6, uint64(m.Time.Nanosecond()/1000), '0') + buf.WriteByte(' ') + + nDigits(buf, 7, uint64(m.Thread), ' ') + buf.WriteByte(' ') + + { + file := m.File + if i := strings.LastIndex(file, "/"); i >= 0 { + file = file[i+1:] + } + buf.WriteString(file) + } + + buf.WriteByte(':') + { + var tmp [19]byte + buf.Write(strconv.AppendInt(tmp[:0], int64(m.Line), 10)) + } + buf.WriteString("] ") + + msgStart := buf.Len() + fmt.Fprintf(buf, format, args...) + if buf.Len() > MaxLogMessageLen-1 { + buf.Truncate(MaxLogMessageLen - 1) + } + msgEnd := buf.Len() + if b := buf.Bytes(); b[len(b)-1] != '\n' { + buf.WriteByte('\n') + } + + for _, s := range sinks { + sn, sErr := s.Emit(m, buf.Bytes()) + if sn > n { + n = sn + } + if sErr != nil && err == nil { + err = sErr + } + } + + if m.Severity == Fatal { + savedM := *m + fatalMessageStore(savedEntry{ + meta: &savedM, + msg: buf.Bytes()[msgStart:msgEnd], + }) + } else { + bufs.Put(bufi) + } + return n, err +} + +const digits = "0123456789" + +// twoDigits formats a zero-prefixed two-digit integer to buf. +func twoDigits(buf *bytes.Buffer, d int) { + buf.WriteByte(digits[(d/10)%10]) + buf.WriteByte(digits[d%10]) +} + +// nDigits formats an n-digit integer to buf, padding with pad on the left. It +// assumes d != 0. +func nDigits(buf *bytes.Buffer, n int, d uint64, pad byte) { + var tmp [20]byte + + cutoff := len(tmp) - n + j := len(tmp) - 1 + for ; d > 0; j-- { + tmp[j] = digits[d%10] + d /= 10 + } + for ; j >= cutoff; j-- { + tmp[j] = pad + } + j++ + buf.Write(tmp[j:]) +} + +// Printf writes a log entry to all registered TextSinks in this package, then +// to all registered StructuredSinks. +// +// The returned n is the maximum across all Emit and Printf calls. +// The returned err is the first non-nil error encountered. +// Sinks that are disabled by configuration should return (0, nil). +func Printf(m *Meta, format string, args ...any) (n int, err error) { + m.Depth++ + n, err = textPrintf(m, TextSinks, format, args...) + + for _, sink := range StructuredSinks { + // TODO: Support TextSinks that implement StackWanter? + if sw, ok := sink.(StackWanter); ok && sw.WantStack(m) { + if m.Stack == nil { + // First, try to find a stacktrace in args, otherwise generate one. + for _, arg := range args { + if stack, ok := arg.(stackdump.Stack); ok { + m.Stack = &stack + break + } + } + if m.Stack == nil { + stack := stackdump.Caller( /* skipDepth = */ m.Depth) + m.Stack = &stack + } + } + } + sn, sErr := sink.Printf(m, format, args...) + if sn > n { + n = sn + } + if sErr != nil && err == nil { + err = sErr + } + } + return n, err +} + +// The sets of sinks to which logs should be written. +// +// These must only be modified during package init, and are read-only thereafter. +var ( + // StructuredSinks is the set of Structured sink instances to which logs + // should be written. + StructuredSinks []Structured + + // TextSinks is the set of Text sink instances to which logs should be + // written. + // + // These are registered separately from Structured sink implementations to + // avoid the need to repeat the work of formatting a message for each Text + // sink that writes it. The package-level Printf function writes to both sets + // independenty, so a given log destination should only register a Structured + // *or* a Text sink (not both). + TextSinks []Text +) + +type savedEntry struct { + meta *Meta + msg []byte +} + +// StructuredTextWrapper is a Structured sink which forwards logs to a set of Text sinks. +// +// The purpose of this sink is to allow applications to intercept logging calls before they are +// serialized and sent to Text sinks. For example, if one needs to redact PII from logging +// arguments before they reach STDERR, one solution would be to do the redacting in a Structured +// sink that forwards logs to a StructuredTextWrapper instance, and make STDERR a child of that +// StructuredTextWrapper instance. This is how one could set this up in their application: +// +// func init() { +// +// wrapper := logsink.StructuredTextWrapper{TextSinks: logsink.TextSinks} +// // sanitizersink will intercept logs and remove PII +// sanitizer := sanitizersink{Sink: &wrapper} +// logsink.StructuredSinks = append(logsink.StructuredSinks, &sanitizer) +// logsink.TextSinks = nil +// +// } +type StructuredTextWrapper struct { + // TextSinks is the set of Text sinks that should receive logs from this + // StructuredTextWrapper instance. + TextSinks []Text +} + +// Printf forwards logs to all Text sinks registered in the StructuredTextWrapper. +func (w *StructuredTextWrapper) Printf(meta *Meta, format string, args ...any) (n int, err error) { + return textPrintf(meta, w.TextSinks, format, args...) +} diff --git a/vendor/github.com/golang/glog/internal/logsink/logsink_fatal.go b/vendor/github.com/golang/glog/internal/logsink/logsink_fatal.go new file mode 100644 index 00000000..3dc269ab --- /dev/null +++ b/vendor/github.com/golang/glog/internal/logsink/logsink_fatal.go @@ -0,0 +1,35 @@ +package logsink + +import ( + "sync/atomic" + "unsafe" +) + +func fatalMessageStore(e savedEntry) { + // Only put a new one in if we haven't assigned before. + atomic.CompareAndSwapPointer(&fatalMessage, nil, unsafe.Pointer(&e)) +} + +var fatalMessage unsafe.Pointer // savedEntry stored with CompareAndSwapPointer + +// FatalMessage returns the Meta and message contents of the first message +// logged with Fatal severity, or false if none has occurred. +func FatalMessage() (*Meta, []byte, bool) { + e := (*savedEntry)(atomic.LoadPointer(&fatalMessage)) + if e == nil { + return nil, nil, false + } + return e.meta, e.msg, true +} + +// DoNotUseRacyFatalMessage is FatalMessage, but worse. +// +//go:norace +//go:nosplit +func DoNotUseRacyFatalMessage() (*Meta, []byte, bool) { + e := (*savedEntry)(fatalMessage) + if e == nil { + return nil, nil, false + } + return e.meta, e.msg, true +} diff --git a/vendor/github.com/golang/glog/internal/stackdump/stackdump.go b/vendor/github.com/golang/glog/internal/stackdump/stackdump.go new file mode 100644 index 00000000..3427c9d6 --- /dev/null +++ b/vendor/github.com/golang/glog/internal/stackdump/stackdump.go @@ -0,0 +1,127 @@ +// Copyright 2023 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package stackdump provides wrappers for runtime.Stack and runtime.Callers +// with uniform support for skipping caller frames. +// +// ⚠ Unlike the functions in the runtime package, these may allocate a +// non-trivial quantity of memory: use them with care. ⚠ +package stackdump + +import ( + "bytes" + "runtime" +) + +// runtimeStackSelfFrames is 1 if runtime.Stack includes the call to +// runtime.Stack itself or 0 if it does not. +// +// As of 2016-04-27, the gccgo compiler includes runtime.Stack but the gc +// compiler does not. +var runtimeStackSelfFrames = func() int { + for n := 1 << 10; n < 1<<20; n *= 2 { + buf := make([]byte, n) + n := runtime.Stack(buf, false) + if bytes.Contains(buf[:n], []byte("runtime.Stack")) { + return 1 + } else if n < len(buf) || bytes.Count(buf, []byte("\n")) >= 3 { + return 0 + } + } + return 0 +}() + +// Stack is a stack dump for a single goroutine. +type Stack struct { + // Text is a representation of the stack dump in a human-readable format. + Text []byte + + // PC is a representation of the stack dump using raw program counter values. + PC []uintptr +} + +func (s Stack) String() string { return string(s.Text) } + +// Caller returns the Stack dump for the calling goroutine, starting skipDepth +// frames before the caller of Caller. (Caller(0) provides a dump starting at +// the caller of this function.) +func Caller(skipDepth int) Stack { + return Stack{ + Text: CallerText(skipDepth + 1), + PC: CallerPC(skipDepth + 1), + } +} + +// CallerText returns a textual dump of the stack starting skipDepth frames before +// the caller. (CallerText(0) provides a dump starting at the caller of this +// function.) +func CallerText(skipDepth int) []byte { + for n := 1 << 10; ; n *= 2 { + buf := make([]byte, n) + n := runtime.Stack(buf, false) + if n < len(buf) { + return pruneFrames(skipDepth+1+runtimeStackSelfFrames, buf[:n]) + } + } +} + +// CallerPC returns a dump of the program counters of the stack starting +// skipDepth frames before the caller. (CallerPC(0) provides a dump starting at +// the caller of this function.) +func CallerPC(skipDepth int) []uintptr { + for n := 1 << 8; ; n *= 2 { + buf := make([]uintptr, n) + n := runtime.Callers(skipDepth+2, buf) + if n < len(buf) { + return buf[:n] + } + } +} + +// pruneFrames removes the topmost skipDepth frames of the first goroutine in a +// textual stack dump. It overwrites the passed-in slice. +// +// If there are fewer than skipDepth frames in the first goroutine's stack, +// pruneFrames prunes it to an empty stack and leaves the remaining contents +// intact. +func pruneFrames(skipDepth int, stack []byte) []byte { + headerLen := 0 + for i, c := range stack { + if c == '\n' { + headerLen = i + 1 + break + } + } + if headerLen == 0 { + return stack // No header line - not a well-formed stack trace. + } + + skipLen := headerLen + skipNewlines := skipDepth * 2 + for ; skipLen < len(stack) && skipNewlines > 0; skipLen++ { + c := stack[skipLen] + if c != '\n' { + continue + } + skipNewlines-- + skipLen++ + if skipNewlines == 0 || skipLen == len(stack) || stack[skipLen] == '\n' { + break + } + } + + pruned := stack[skipLen-headerLen:] + copy(pruned, stack[:headerLen]) + return pruned +} diff --git a/vendor/github.com/google/go-querystring/query/encode.go b/vendor/github.com/google/go-querystring/query/encode.go index 37080b19..91198f81 100644 --- a/vendor/github.com/google/go-querystring/query/encode.go +++ b/vendor/github.com/google/go-querystring/query/encode.go @@ -51,8 +51,8 @@ type Encoder interface { // - the field is empty and its tag specifies the "omitempty" option // // The empty values are false, 0, any nil pointer or interface value, any array -// slice, map, or string of length zero, and any time.Time that returns true -// for IsZero(). +// slice, map, or string of length zero, and any type (such as time.Time) that +// returns true for IsZero(). // // The URL parameter name defaults to the struct field name but can be // specified in the struct field's tag value. The "url" key in the struct @@ -82,7 +82,14 @@ type Encoder interface { // // time.Time values default to encoding as RFC3339 timestamps. Including the // "unix" option signals that the field should be encoded as a Unix time (see -// time.Unix()) +// time.Unix()). The "unixmilli" and "unixnano" options will encode the number +// of milliseconds and nanoseconds, respectively, since January 1, 1970 (see +// time.UnixNano()). Including the "layout" struct tag (separate from the +// "url" tag) will use the value of the "layout" tag as a layout passed to +// time.Format. For example: +// +// // Encode a time.Time as YYYY-MM-DD +// Field time.Time `layout:"2006-01-02"` // // Slice and Array values default to encoding as multiple URL values of the // same name. Including the "comma" option signals that the field should be @@ -92,7 +99,13 @@ type Encoder interface { // Including the "brackets" option signals that the multiple URL values should // have "[]" appended to the value name. "numbered" will append a number to // the end of each incidence of the value name, example: -// name0=value0&name1=value1, etc. +// name0=value0&name1=value1, etc. Including the "del" struct tag (separate +// from the "url" tag) will use the value of the "del" tag as the delimiter. +// For example: +// +// // Encode a slice of bools as ints ("1" for true, "0" for false), +// // separated by exclamation points "!". +// Field []bool `url:",int" del:"!"` // // Anonymous struct fields are usually encoded as if their inner exported // fields were fields in the outer struct, subject to the standard Go @@ -151,11 +164,15 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error { continue } name, opts := parseTag(tag) + if name == "" { - if sf.Anonymous && sv.Kind() == reflect.Struct { - // save embedded struct for later processing - embedded = append(embedded, sv) - continue + if sf.Anonymous { + v := reflect.Indirect(sv) + if v.IsValid() && v.Kind() == reflect.Struct { + // save embedded struct for later processing + embedded = append(embedded, v) + continue + } } name = sf.Name @@ -170,7 +187,9 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error { } if sv.Type().Implements(encoderType) { - if !reflect.Indirect(sv).IsValid() { + // if sv is a nil pointer and the custom encoder is defined on a non-pointer + // method receiver, set sv to the zero value of the underlying type + if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(encoderType) { sv = reflect.New(sv.Type().Elem()) } @@ -181,28 +200,38 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error { continue } + // recursively dereference pointers. break on nil pointers + for sv.Kind() == reflect.Ptr { + if sv.IsNil() { + break + } + sv = sv.Elem() + } + if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { - var del byte + var del string if opts.Contains("comma") { - del = ',' + del = "," } else if opts.Contains("space") { - del = ' ' + del = " " } else if opts.Contains("semicolon") { - del = ';' + del = ";" } else if opts.Contains("brackets") { name = name + "[]" + } else { + del = sf.Tag.Get("del") } - if del != 0 { + if del != "" { s := new(bytes.Buffer) first := true for i := 0; i < sv.Len(); i++ { if first { first = false } else { - s.WriteByte(del) + s.WriteString(del) } - s.WriteString(valueString(sv.Index(i), opts)) + s.WriteString(valueString(sv.Index(i), opts, sf)) } values.Add(name, s.String()) } else { @@ -211,30 +240,25 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error { if opts.Contains("numbered") { k = fmt.Sprintf("%s%d", name, i) } - values.Add(k, valueString(sv.Index(i), opts)) + values.Add(k, valueString(sv.Index(i), opts, sf)) } } continue } - for sv.Kind() == reflect.Ptr { - if sv.IsNil() { - break - } - sv = sv.Elem() - } - if sv.Type() == timeType { - values.Add(name, valueString(sv, opts)) + values.Add(name, valueString(sv, opts, sf)) continue } if sv.Kind() == reflect.Struct { - reflectValue(values, sv, name) + if err := reflectValue(values, sv, name); err != nil { + return err + } continue } - values.Add(name, valueString(sv, opts)) + values.Add(name, valueString(sv, opts, sf)) } for _, f := range embedded { @@ -247,7 +271,7 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error { } // valueString returns the string representation of a value. -func valueString(v reflect.Value, opts tagOptions) string { +func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string { for v.Kind() == reflect.Ptr { if v.IsNil() { return "" @@ -267,6 +291,15 @@ func valueString(v reflect.Value, opts tagOptions) string { if opts.Contains("unix") { return strconv.FormatInt(t.Unix(), 10) } + if opts.Contains("unixmilli") { + return strconv.FormatInt((t.UnixNano() / 1e6), 10) + } + if opts.Contains("unixnano") { + return strconv.FormatInt(t.UnixNano(), 10) + } + if layout := sf.Tag.Get("layout"); layout != "" { + return t.Format(layout) + } return t.Format(time.RFC3339) } @@ -291,8 +324,12 @@ func isEmptyValue(v reflect.Value) bool { return v.IsNil() } - if v.Type() == timeType { - return v.Interface().(time.Time).IsZero() + type zeroable interface { + IsZero() bool + } + + if z, ok := v.Interface().(zeroable); ok { + return z.IsZero() } return false diff --git a/vendor/github.com/google/uuid/CHANGELOG.md b/vendor/github.com/google/uuid/CHANGELOG.md new file mode 100644 index 00000000..2bd78667 --- /dev/null +++ b/vendor/github.com/google/uuid/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +## [1.3.1](https://github.com/google/uuid/compare/v1.3.0...v1.3.1) (2023-08-18) + + +### Bug Fixes + +* Use .EqualFold() to parse urn prefixed UUIDs ([#118](https://github.com/google/uuid/issues/118)) ([574e687](https://github.com/google/uuid/commit/574e6874943741fb99d41764c705173ada5293f0)) + +## Changelog diff --git a/vendor/github.com/google/uuid/CONTRIBUTING.md b/vendor/github.com/google/uuid/CONTRIBUTING.md new file mode 100644 index 00000000..55668887 --- /dev/null +++ b/vendor/github.com/google/uuid/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# How to contribute + +We definitely welcome patches and contribution to this project! + +### Tips + +Commits must be formatted according to the [Conventional Commits Specification](https://www.conventionalcommits.org). + +Always try to include a test case! If it is not possible or not necessary, +please explain why in the pull request description. + +### Releasing + +Commits that would precipitate a SemVer change, as desrcibed in the Conventional +Commits Specification, will trigger [`release-please`](https://github.com/google-github-actions/release-please-action) +to create a release candidate pull request. Once submitted, `release-please` +will create a release. + +For tips on how to work with `release-please`, see its documentation. + +### Legal requirements + +In order to protect both you and ourselves, you will need to sign the +[Contributor License Agreement](https://cla.developers.google.com/clas). + +You may have already signed it for other Google projects. diff --git a/vendor/github.com/google/uuid/CONTRIBUTORS b/vendor/github.com/google/uuid/CONTRIBUTORS new file mode 100644 index 00000000..b4bb97f6 --- /dev/null +++ b/vendor/github.com/google/uuid/CONTRIBUTORS @@ -0,0 +1,9 @@ +Paul Borman +bmatsuo +shawnps +theory +jboverfelt +dsymonds +cd1 +wallclockbuilder +dansouza diff --git a/vendor/github.com/google/uuid/LICENSE b/vendor/github.com/google/uuid/LICENSE new file mode 100644 index 00000000..5dc68268 --- /dev/null +++ b/vendor/github.com/google/uuid/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009,2014 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/google/uuid/README.md b/vendor/github.com/google/uuid/README.md new file mode 100644 index 00000000..3e9a6188 --- /dev/null +++ b/vendor/github.com/google/uuid/README.md @@ -0,0 +1,21 @@ +# uuid +The uuid package generates and inspects UUIDs based on +[RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) +and DCE 1.1: Authentication and Security Services. + +This package is based on the github.com/pborman/uuid package (previously named +code.google.com/p/go-uuid). It differs from these earlier packages in that +a UUID is a 16 byte array rather than a byte slice. One loss due to this +change is the ability to represent an invalid UUID (vs a NIL UUID). + +###### Install +```sh +go get github.com/google/uuid +``` + +###### Documentation +[![Go Reference](https://pkg.go.dev/badge/github.com/google/uuid.svg)](https://pkg.go.dev/github.com/google/uuid) + +Full `go doc` style documentation for the package can be viewed online without +installing this package by using the GoDoc site here: +http://pkg.go.dev/github.com/google/uuid diff --git a/vendor/github.com/google/uuid/dce.go b/vendor/github.com/google/uuid/dce.go new file mode 100644 index 00000000..fa820b9d --- /dev/null +++ b/vendor/github.com/google/uuid/dce.go @@ -0,0 +1,80 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "fmt" + "os" +) + +// A Domain represents a Version 2 domain +type Domain byte + +// Domain constants for DCE Security (Version 2) UUIDs. +const ( + Person = Domain(0) + Group = Domain(1) + Org = Domain(2) +) + +// NewDCESecurity returns a DCE Security (Version 2) UUID. +// +// The domain should be one of Person, Group or Org. +// On a POSIX system the id should be the users UID for the Person +// domain and the users GID for the Group. The meaning of id for +// the domain Org or on non-POSIX systems is site defined. +// +// For a given domain/id pair the same token may be returned for up to +// 7 minutes and 10 seconds. +func NewDCESecurity(domain Domain, id uint32) (UUID, error) { + uuid, err := NewUUID() + if err == nil { + uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2 + uuid[9] = byte(domain) + binary.BigEndian.PutUint32(uuid[0:], id) + } + return uuid, err +} + +// NewDCEPerson returns a DCE Security (Version 2) UUID in the person +// domain with the id returned by os.Getuid. +// +// NewDCESecurity(Person, uint32(os.Getuid())) +func NewDCEPerson() (UUID, error) { + return NewDCESecurity(Person, uint32(os.Getuid())) +} + +// NewDCEGroup returns a DCE Security (Version 2) UUID in the group +// domain with the id returned by os.Getgid. +// +// NewDCESecurity(Group, uint32(os.Getgid())) +func NewDCEGroup() (UUID, error) { + return NewDCESecurity(Group, uint32(os.Getgid())) +} + +// Domain returns the domain for a Version 2 UUID. Domains are only defined +// for Version 2 UUIDs. +func (uuid UUID) Domain() Domain { + return Domain(uuid[9]) +} + +// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2 +// UUIDs. +func (uuid UUID) ID() uint32 { + return binary.BigEndian.Uint32(uuid[0:4]) +} + +func (d Domain) String() string { + switch d { + case Person: + return "Person" + case Group: + return "Group" + case Org: + return "Org" + } + return fmt.Sprintf("Domain%d", int(d)) +} diff --git a/vendor/github.com/google/uuid/doc.go b/vendor/github.com/google/uuid/doc.go new file mode 100644 index 00000000..5b8a4b9a --- /dev/null +++ b/vendor/github.com/google/uuid/doc.go @@ -0,0 +1,12 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package uuid generates and inspects UUIDs. +// +// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security +// Services. +// +// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to +// maps or compared directly. +package uuid diff --git a/vendor/github.com/google/uuid/hash.go b/vendor/github.com/google/uuid/hash.go new file mode 100644 index 00000000..b404f4be --- /dev/null +++ b/vendor/github.com/google/uuid/hash.go @@ -0,0 +1,53 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "crypto/md5" + "crypto/sha1" + "hash" +) + +// Well known namespace IDs and UUIDs +var ( + NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) + Nil UUID // empty UUID, all zeros +) + +// NewHash returns a new UUID derived from the hash of space concatenated with +// data generated by h. The hash should be at least 16 byte in length. The +// first 16 bytes of the hash are used to form the UUID. The version of the +// UUID will be the lower 4 bits of version. NewHash is used to implement +// NewMD5 and NewSHA1. +func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { + h.Reset() + h.Write(space[:]) //nolint:errcheck + h.Write(data) //nolint:errcheck + s := h.Sum(nil) + var uuid UUID + copy(uuid[:], s) + uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) + uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant + return uuid +} + +// NewMD5 returns a new MD5 (Version 3) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(md5.New(), space, data, 3) +func NewMD5(space UUID, data []byte) UUID { + return NewHash(md5.New(), space, data, 3) +} + +// NewSHA1 returns a new SHA1 (Version 5) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(sha1.New(), space, data, 5) +func NewSHA1(space UUID, data []byte) UUID { + return NewHash(sha1.New(), space, data, 5) +} diff --git a/vendor/github.com/google/uuid/marshal.go b/vendor/github.com/google/uuid/marshal.go new file mode 100644 index 00000000..14bd3407 --- /dev/null +++ b/vendor/github.com/google/uuid/marshal.go @@ -0,0 +1,38 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "fmt" + +// MarshalText implements encoding.TextMarshaler. +func (uuid UUID) MarshalText() ([]byte, error) { + var js [36]byte + encodeHex(js[:], uuid) + return js[:], nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (uuid *UUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err != nil { + return err + } + *uuid = id + return nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (uuid UUID) MarshalBinary() ([]byte, error) { + return uuid[:], nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (uuid *UUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(uuid[:], data) + return nil +} diff --git a/vendor/github.com/google/uuid/node.go b/vendor/github.com/google/uuid/node.go new file mode 100644 index 00000000..d651a2b0 --- /dev/null +++ b/vendor/github.com/google/uuid/node.go @@ -0,0 +1,90 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "sync" +) + +var ( + nodeMu sync.Mutex + ifname string // name of interface being used + nodeID [6]byte // hardware for version 1 UUIDs + zeroID [6]byte // nodeID with only 0's +) + +// NodeInterface returns the name of the interface from which the NodeID was +// derived. The interface "user" is returned if the NodeID was set by +// SetNodeID. +func NodeInterface() string { + defer nodeMu.Unlock() + nodeMu.Lock() + return ifname +} + +// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs. +// If name is "" then the first usable interface found will be used or a random +// Node ID will be generated. If a named interface cannot be found then false +// is returned. +// +// SetNodeInterface never fails when name is "". +func SetNodeInterface(name string) bool { + defer nodeMu.Unlock() + nodeMu.Lock() + return setNodeInterface(name) +} + +func setNodeInterface(name string) bool { + iname, addr := getHardwareInterface(name) // null implementation for js + if iname != "" && addr != nil { + ifname = iname + copy(nodeID[:], addr) + return true + } + + // We found no interfaces with a valid hardware address. If name + // does not specify a specific interface generate a random Node ID + // (section 4.1.6) + if name == "" { + ifname = "random" + randomBits(nodeID[:]) + return true + } + return false +} + +// NodeID returns a slice of a copy of the current Node ID, setting the Node ID +// if not already set. +func NodeID() []byte { + defer nodeMu.Unlock() + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + nid := nodeID + return nid[:] +} + +// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes +// of id are used. If id is less than 6 bytes then false is returned and the +// Node ID is not set. +func SetNodeID(id []byte) bool { + if len(id) < 6 { + return false + } + defer nodeMu.Unlock() + nodeMu.Lock() + copy(nodeID[:], id) + ifname = "user" + return true +} + +// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is +// not valid. The NodeID is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) NodeID() []byte { + var node [6]byte + copy(node[:], uuid[10:]) + return node[:] +} diff --git a/vendor/github.com/google/uuid/node_js.go b/vendor/github.com/google/uuid/node_js.go new file mode 100644 index 00000000..b2a0bc87 --- /dev/null +++ b/vendor/github.com/google/uuid/node_js.go @@ -0,0 +1,12 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build js + +package uuid + +// getHardwareInterface returns nil values for the JS version of the code. +// This removes the "net" dependency, because it is not used in the browser. +// Using the "net" library inflates the size of the transpiled JS code by 673k bytes. +func getHardwareInterface(name string) (string, []byte) { return "", nil } diff --git a/vendor/github.com/google/uuid/node_net.go b/vendor/github.com/google/uuid/node_net.go new file mode 100644 index 00000000..0cbbcddb --- /dev/null +++ b/vendor/github.com/google/uuid/node_net.go @@ -0,0 +1,33 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !js + +package uuid + +import "net" + +var interfaces []net.Interface // cached list of interfaces + +// getHardwareInterface returns the name and hardware address of interface name. +// If name is "" then the name and hardware address of one of the system's +// interfaces is returned. If no interfaces are found (name does not exist or +// there are no interfaces) then "", nil is returned. +// +// Only addresses of at least 6 bytes are returned. +func getHardwareInterface(name string) (string, []byte) { + if interfaces == nil { + var err error + interfaces, err = net.Interfaces() + if err != nil { + return "", nil + } + } + for _, ifs := range interfaces { + if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { + return ifs.Name, ifs.HardwareAddr + } + } + return "", nil +} diff --git a/vendor/github.com/google/uuid/null.go b/vendor/github.com/google/uuid/null.go new file mode 100644 index 00000000..d7fcbf28 --- /dev/null +++ b/vendor/github.com/google/uuid/null.go @@ -0,0 +1,118 @@ +// Copyright 2021 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "database/sql/driver" + "encoding/json" + "fmt" +) + +var jsonNull = []byte("null") + +// NullUUID represents a UUID that may be null. +// NullUUID implements the SQL driver.Scanner interface so +// it can be used as a scan destination: +// +// var u uuid.NullUUID +// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u) +// ... +// if u.Valid { +// // use u.UUID +// } else { +// // NULL value +// } +// +type NullUUID struct { + UUID UUID + Valid bool // Valid is true if UUID is not NULL +} + +// Scan implements the SQL driver.Scanner interface. +func (nu *NullUUID) Scan(value interface{}) error { + if value == nil { + nu.UUID, nu.Valid = Nil, false + return nil + } + + err := nu.UUID.Scan(value) + if err != nil { + nu.Valid = false + return err + } + + nu.Valid = true + return nil +} + +// Value implements the driver Valuer interface. +func (nu NullUUID) Value() (driver.Value, error) { + if !nu.Valid { + return nil, nil + } + // Delegate to UUID Value function + return nu.UUID.Value() +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (nu NullUUID) MarshalBinary() ([]byte, error) { + if nu.Valid { + return nu.UUID[:], nil + } + + return []byte(nil), nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (nu *NullUUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(nu.UUID[:], data) + nu.Valid = true + return nil +} + +// MarshalText implements encoding.TextMarshaler. +func (nu NullUUID) MarshalText() ([]byte, error) { + if nu.Valid { + return nu.UUID.MarshalText() + } + + return jsonNull, nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (nu *NullUUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err != nil { + nu.Valid = false + return err + } + nu.UUID = id + nu.Valid = true + return nil +} + +// MarshalJSON implements json.Marshaler. +func (nu NullUUID) MarshalJSON() ([]byte, error) { + if nu.Valid { + return json.Marshal(nu.UUID) + } + + return jsonNull, nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (nu *NullUUID) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, jsonNull) { + *nu = NullUUID{} + return nil // valid null UUID + } + err := json.Unmarshal(data, &nu.UUID) + nu.Valid = err == nil + return err +} diff --git a/vendor/github.com/google/uuid/sql.go b/vendor/github.com/google/uuid/sql.go new file mode 100644 index 00000000..2e02ec06 --- /dev/null +++ b/vendor/github.com/google/uuid/sql.go @@ -0,0 +1,59 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements sql.Scanner so UUIDs can be read from databases transparently. +// Currently, database types that map to string and []byte are supported. Please +// consult database-specific driver documentation for matching types. +func (uuid *UUID) Scan(src interface{}) error { + switch src := src.(type) { + case nil: + return nil + + case string: + // if an empty UUID comes from a table, we return a null UUID + if src == "" { + return nil + } + + // see Parse for required string format + u, err := Parse(src) + if err != nil { + return fmt.Errorf("Scan: %v", err) + } + + *uuid = u + + case []byte: + // if an empty UUID comes from a table, we return a null UUID + if len(src) == 0 { + return nil + } + + // assumes a simple slice of bytes if 16 bytes + // otherwise attempts to parse + if len(src) != 16 { + return uuid.Scan(string(src)) + } + copy((*uuid)[:], src) + + default: + return fmt.Errorf("Scan: unable to scan type %T into UUID", src) + } + + return nil +} + +// Value implements sql.Valuer so that UUIDs can be written to databases +// transparently. Currently, UUIDs map to strings. Please consult +// database-specific driver documentation for matching types. +func (uuid UUID) Value() (driver.Value, error) { + return uuid.String(), nil +} diff --git a/vendor/github.com/google/uuid/time.go b/vendor/github.com/google/uuid/time.go new file mode 100644 index 00000000..e6ef06cd --- /dev/null +++ b/vendor/github.com/google/uuid/time.go @@ -0,0 +1,123 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "sync" + "time" +) + +// A Time represents a time as the number of 100's of nanoseconds since 15 Oct +// 1582. +type Time int64 + +const ( + lillian = 2299160 // Julian day of 15 Oct 1582 + unix = 2440587 // Julian day of 1 Jan 1970 + epoch = unix - lillian // Days between epochs + g1582 = epoch * 86400 // seconds between epochs + g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs +) + +var ( + timeMu sync.Mutex + lasttime uint64 // last time we returned + clockSeq uint16 // clock sequence for this run + + timeNow = time.Now // for testing +) + +// UnixTime converts t the number of seconds and nanoseconds using the Unix +// epoch of 1 Jan 1970. +func (t Time) UnixTime() (sec, nsec int64) { + sec = int64(t - g1582ns100) + nsec = (sec % 10000000) * 100 + sec /= 10000000 + return sec, nsec +} + +// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and +// clock sequence as well as adjusting the clock sequence as needed. An error +// is returned if the current time cannot be determined. +func GetTime() (Time, uint16, error) { + defer timeMu.Unlock() + timeMu.Lock() + return getTime() +} + +func getTime() (Time, uint16, error) { + t := timeNow() + + // If we don't have a clock sequence already, set one. + if clockSeq == 0 { + setClockSequence(-1) + } + now := uint64(t.UnixNano()/100) + g1582ns100 + + // If time has gone backwards with this clock sequence then we + // increment the clock sequence + if now <= lasttime { + clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000 + } + lasttime = now + return Time(now), clockSeq, nil +} + +// ClockSequence returns the current clock sequence, generating one if not +// already set. The clock sequence is only used for Version 1 UUIDs. +// +// The uuid package does not use global static storage for the clock sequence or +// the last time a UUID was generated. Unless SetClockSequence is used, a new +// random clock sequence is generated the first time a clock sequence is +// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) +func ClockSequence() int { + defer timeMu.Unlock() + timeMu.Lock() + return clockSequence() +} + +func clockSequence() int { + if clockSeq == 0 { + setClockSequence(-1) + } + return int(clockSeq & 0x3fff) +} + +// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to +// -1 causes a new sequence to be generated. +func SetClockSequence(seq int) { + defer timeMu.Unlock() + timeMu.Lock() + setClockSequence(seq) +} + +func setClockSequence(seq int) { + if seq == -1 { + var b [2]byte + randomBits(b[:]) // clock sequence + seq = int(b[0])<<8 | int(b[1]) + } + oldSeq := clockSeq + clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant + if oldSeq != clockSeq { + lasttime = 0 + } +} + +// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in +// uuid. The time is only defined for version 1 and 2 UUIDs. +func (uuid UUID) Time() Time { + time := int64(binary.BigEndian.Uint32(uuid[0:4])) + time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 + time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 + return Time(time) +} + +// ClockSequence returns the clock sequence encoded in uuid. +// The clock sequence is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) ClockSequence() int { + return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff +} diff --git a/vendor/github.com/google/uuid/util.go b/vendor/github.com/google/uuid/util.go new file mode 100644 index 00000000..5ea6c737 --- /dev/null +++ b/vendor/github.com/google/uuid/util.go @@ -0,0 +1,43 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "io" +) + +// randomBits completely fills slice b with random data. +func randomBits(b []byte) { + if _, err := io.ReadFull(rander, b); err != nil { + panic(err.Error()) // rand should never fail + } +} + +// xvalues returns the value of a byte as a hexadecimal digit or 255. +var xvalues = [256]byte{ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +} + +// xtob converts hex characters x1 and x2 into a byte. +func xtob(x1, x2 byte) (byte, bool) { + b1 := xvalues[x1] + b2 := xvalues[x2] + return (b1 << 4) | b2, b1 != 255 && b2 != 255 +} diff --git a/vendor/github.com/google/uuid/uuid.go b/vendor/github.com/google/uuid/uuid.go new file mode 100644 index 00000000..a56138cc --- /dev/null +++ b/vendor/github.com/google/uuid/uuid.go @@ -0,0 +1,296 @@ +// Copyright 2018 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" + "sync" +) + +// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC +// 4122. +type UUID [16]byte + +// A Version represents a UUID's version. +type Version byte + +// A Variant represents a UUID's variant. +type Variant byte + +// Constants returned by Variant. +const ( + Invalid = Variant(iota) // Invalid UUID + RFC4122 // The variant specified in RFC4122 + Reserved // Reserved, NCS backward compatibility. + Microsoft // Reserved, Microsoft Corporation backward compatibility. + Future // Reserved for future definition. +) + +const randPoolSize = 16 * 16 + +var ( + rander = rand.Reader // random function + poolEnabled = false + poolMu sync.Mutex + poolPos = randPoolSize // protected with poolMu + pool [randPoolSize]byte // protected with poolMu +) + +type invalidLengthError struct{ len int } + +func (err invalidLengthError) Error() string { + return fmt.Sprintf("invalid UUID length: %d", err.len) +} + +// IsInvalidLengthError is matcher function for custom error invalidLengthError +func IsInvalidLengthError(err error) bool { + _, ok := err.(invalidLengthError) + return ok +} + +// Parse decodes s into a UUID or returns an error. Both the standard UUID +// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the +// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex +// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. +func Parse(s string) (UUID, error) { + var uuid UUID + switch len(s) { + // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36: + + // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: + if !strings.EqualFold(s[:9], "urn:uuid:") { + return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) + } + s = s[9:] + + // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + case 36 + 2: + s = s[1:] + + // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + case 32: + var ok bool + for i := range uuid { + uuid[i], ok = xtob(s[i*2], s[i*2+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, invalidLengthError{len(s)} + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34, + } { + v, ok := xtob(s[x], s[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// ParseBytes is like Parse, except it parses a byte slice instead of a string. +func ParseBytes(b []byte) (UUID, error) { + var uuid UUID + switch len(b) { + case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if !bytes.EqualFold(b[:9], []byte("urn:uuid:")) { + return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) + } + b = b[9:] + case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + b = b[1:] + case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + var ok bool + for i := 0; i < 32; i += 2 { + uuid[i/2], ok = xtob(b[i], b[i+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, invalidLengthError{len(b)} + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34, + } { + v, ok := xtob(b[x], b[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// MustParse is like Parse but panics if the string cannot be parsed. +// It simplifies safe initialization of global variables holding compiled UUIDs. +func MustParse(s string) UUID { + uuid, err := Parse(s) + if err != nil { + panic(`uuid: Parse(` + s + `): ` + err.Error()) + } + return uuid +} + +// FromBytes creates a new UUID from a byte slice. Returns an error if the slice +// does not have a length of 16. The bytes are copied from the slice. +func FromBytes(b []byte) (uuid UUID, err error) { + err = uuid.UnmarshalBinary(b) + return uuid, err +} + +// Must returns uuid if err is nil and panics otherwise. +func Must(uuid UUID, err error) UUID { + if err != nil { + panic(err) + } + return uuid +} + +// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// , or "" if uuid is invalid. +func (uuid UUID) String() string { + var buf [36]byte + encodeHex(buf[:], uuid) + return string(buf[:]) +} + +// URN returns the RFC 2141 URN form of uuid, +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid. +func (uuid UUID) URN() string { + var buf [36 + 9]byte + copy(buf[:], "urn:uuid:") + encodeHex(buf[9:], uuid) + return string(buf[:]) +} + +func encodeHex(dst []byte, uuid UUID) { + hex.Encode(dst, uuid[:4]) + dst[8] = '-' + hex.Encode(dst[9:13], uuid[4:6]) + dst[13] = '-' + hex.Encode(dst[14:18], uuid[6:8]) + dst[18] = '-' + hex.Encode(dst[19:23], uuid[8:10]) + dst[23] = '-' + hex.Encode(dst[24:], uuid[10:]) +} + +// Variant returns the variant encoded in uuid. +func (uuid UUID) Variant() Variant { + switch { + case (uuid[8] & 0xc0) == 0x80: + return RFC4122 + case (uuid[8] & 0xe0) == 0xc0: + return Microsoft + case (uuid[8] & 0xe0) == 0xe0: + return Future + default: + return Reserved + } +} + +// Version returns the version of uuid. +func (uuid UUID) Version() Version { + return Version(uuid[6] >> 4) +} + +func (v Version) String() string { + if v > 15 { + return fmt.Sprintf("BAD_VERSION_%d", v) + } + return fmt.Sprintf("VERSION_%d", v) +} + +func (v Variant) String() string { + switch v { + case RFC4122: + return "RFC4122" + case Reserved: + return "Reserved" + case Microsoft: + return "Microsoft" + case Future: + return "Future" + case Invalid: + return "Invalid" + } + return fmt.Sprintf("BadVariant%d", int(v)) +} + +// SetRand sets the random number generator to r, which implements io.Reader. +// If r.Read returns an error when the package requests random data then +// a panic will be issued. +// +// Calling SetRand with nil sets the random number generator to the default +// generator. +func SetRand(r io.Reader) { + if r == nil { + rander = rand.Reader + return + } + rander = r +} + +// EnableRandPool enables internal randomness pool used for Random +// (Version 4) UUID generation. The pool contains random bytes read from +// the random number generator on demand in batches. Enabling the pool +// may improve the UUID generation throughput significantly. +// +// Since the pool is stored on the Go heap, this feature may be a bad fit +// for security sensitive applications. +// +// Both EnableRandPool and DisableRandPool are not thread-safe and should +// only be called when there is no possibility that New or any other +// UUID Version 4 generation function will be called concurrently. +func EnableRandPool() { + poolEnabled = true +} + +// DisableRandPool disables the randomness pool if it was previously +// enabled with EnableRandPool. +// +// Both EnableRandPool and DisableRandPool are not thread-safe and should +// only be called when there is no possibility that New or any other +// UUID Version 4 generation function will be called concurrently. +func DisableRandPool() { + poolEnabled = false + defer poolMu.Unlock() + poolMu.Lock() + poolPos = randPoolSize +} diff --git a/vendor/github.com/google/uuid/version1.go b/vendor/github.com/google/uuid/version1.go new file mode 100644 index 00000000..46310962 --- /dev/null +++ b/vendor/github.com/google/uuid/version1.go @@ -0,0 +1,44 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" +) + +// NewUUID returns a Version 1 UUID based on the current NodeID and clock +// sequence, and the current time. If the NodeID has not been set by SetNodeID +// or SetNodeInterface then it will be set automatically. If the NodeID cannot +// be set NewUUID returns nil. If clock sequence has not been set by +// SetClockSequence then it will be set automatically. If GetTime fails to +// return the current NewUUID returns nil and an error. +// +// In most cases, New should be used. +func NewUUID() (UUID, error) { + var uuid UUID + now, seq, err := GetTime() + if err != nil { + return uuid, err + } + + timeLow := uint32(now & 0xffffffff) + timeMid := uint16((now >> 32) & 0xffff) + timeHi := uint16((now >> 48) & 0x0fff) + timeHi |= 0x1000 // Version 1 + + binary.BigEndian.PutUint32(uuid[0:], timeLow) + binary.BigEndian.PutUint16(uuid[4:], timeMid) + binary.BigEndian.PutUint16(uuid[6:], timeHi) + binary.BigEndian.PutUint16(uuid[8:], seq) + + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + copy(uuid[10:], nodeID[:]) + nodeMu.Unlock() + + return uuid, nil +} diff --git a/vendor/github.com/google/uuid/version4.go b/vendor/github.com/google/uuid/version4.go new file mode 100644 index 00000000..7697802e --- /dev/null +++ b/vendor/github.com/google/uuid/version4.go @@ -0,0 +1,76 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "io" + +// New creates a new random UUID or panics. New is equivalent to +// the expression +// +// uuid.Must(uuid.NewRandom()) +func New() UUID { + return Must(NewRandom()) +} + +// NewString creates a new random UUID and returns it as a string or panics. +// NewString is equivalent to the expression +// +// uuid.New().String() +func NewString() string { + return Must(NewRandom()).String() +} + +// NewRandom returns a Random (Version 4) UUID. +// +// The strength of the UUIDs is based on the strength of the crypto/rand +// package. +// +// Uses the randomness pool if it was enabled with EnableRandPool. +// +// A note about uniqueness derived from the UUID Wikipedia entry: +// +// Randomly generated UUIDs have 122 random bits. One's annual risk of being +// hit by a meteorite is estimated to be one chance in 17 billion, that +// means the probability is about 0.00000000006 (6 × 10−11), +// equivalent to the odds of creating a few tens of trillions of UUIDs in a +// year and having one duplicate. +func NewRandom() (UUID, error) { + if !poolEnabled { + return NewRandomFromReader(rander) + } + return newRandomFromPool() +} + +// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader. +func NewRandomFromReader(r io.Reader) (UUID, error) { + var uuid UUID + _, err := io.ReadFull(r, uuid[:]) + if err != nil { + return Nil, err + } + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} + +func newRandomFromPool() (UUID, error) { + var uuid UUID + poolMu.Lock() + if poolPos == randPoolSize { + _, err := io.ReadFull(rander, pool[:]) + if err != nil { + poolMu.Unlock() + return Nil, err + } + poolPos = 0 + } + copy(uuid[:], pool[poolPos:(poolPos+16)]) + poolPos += 16 + poolMu.Unlock() + + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} diff --git a/vendor/github.com/gorilla/css/LICENSE b/vendor/github.com/gorilla/css/LICENSE index bee2a059..ee0d53ce 100644 --- a/vendor/github.com/gorilla/css/LICENSE +++ b/vendor/github.com/gorilla/css/LICENSE @@ -1,27 +1,28 @@ -Copyright (c) 2013, Gorilla web toolkit -All rights reserved. +Copyright (c) 2023 The Gorilla Authors. All rights reserved. -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: - Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. - Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - Neither the name of the {organization} nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/css/scanner/scanner.go b/vendor/github.com/gorilla/css/scanner/scanner.go index 23fa7404..25a7c657 100644 --- a/vendor/github.com/gorilla/css/scanner/scanner.go +++ b/vendor/github.com/gorilla/css/scanner/scanner.go @@ -191,7 +191,11 @@ func init() { // New returns a new CSS scanner for the given input. func New(input string) *Scanner { // Normalize newlines. + // https://www.w3.org/TR/css-syntax-3/#input-preprocessing input = strings.Replace(input, "\r\n", "\n", -1) + input = strings.Replace(input, "\r", "\n", -1) + input = strings.Replace(input, "\f", "\n", -1) + input = strings.Replace(input, "\u0000", "\ufffd", -1) return &Scanner{ input: input, row: 1, @@ -232,7 +236,7 @@ func (s *Scanner) Next() *Token { // shortcut before testing multiple regexps. input := s.input[s.pos:] switch input[0] { - case '\t', '\n', '\f', '\r', ' ': + case '\t', '\n', ' ': // Whitespace. return s.emitToken(TokenS, matchers[TokenS].FindString(input)) case '.': diff --git a/vendor/github.com/gorilla/handlers/.editorconfig b/vendor/github.com/gorilla/handlers/.editorconfig new file mode 100644 index 00000000..c6b74c3e --- /dev/null +++ b/vendor/github.com/gorilla/handlers/.editorconfig @@ -0,0 +1,20 @@ +; https://editorconfig.org/ + +root = true + +[*] +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[{Makefile,go.mod,go.sum,*.go,.gitmodules}] +indent_style = tab +indent_size = 4 + +[*.md] +indent_size = 4 +trim_trailing_whitespace = false + +eclint_indent_style = unset \ No newline at end of file diff --git a/vendor/github.com/gorilla/handlers/.gitignore b/vendor/github.com/gorilla/handlers/.gitignore new file mode 100644 index 00000000..577a89e8 --- /dev/null +++ b/vendor/github.com/gorilla/handlers/.gitignore @@ -0,0 +1,2 @@ +# Output of the go test coverage tool +coverage.coverprofile diff --git a/vendor/github.com/gorilla/handlers/LICENSE b/vendor/github.com/gorilla/handlers/LICENSE index 66ea3c8a..bb9d80bc 100644 --- a/vendor/github.com/gorilla/handlers/LICENSE +++ b/vendor/github.com/gorilla/handlers/LICENSE @@ -1,22 +1,27 @@ -Copyright (c) 2013 The Gorilla Handlers Authors. All rights reserved. +Copyright (c) 2023 The Gorilla Authors. All rights reserved. Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +modification, are permitted provided that the following conditions are +met: - Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. - Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/handlers/Makefile b/vendor/github.com/gorilla/handlers/Makefile new file mode 100644 index 00000000..003b784f --- /dev/null +++ b/vendor/github.com/gorilla/handlers/Makefile @@ -0,0 +1,34 @@ +GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '') +GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +GO_SEC=$(shell which gosec 2> /dev/null || echo '') +GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest + +GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '') +GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest + +.PHONY: verify +verify: sec govulncheck lint test + +.PHONY: lint +lint: + $(if $(GO_LINT), ,go install $(GO_LINT_URI)) + @echo "##### Running golangci-lint #####" + golangci-lint run -v + +.PHONY: sec +sec: + $(if $(GO_SEC), ,go install $(GO_SEC_URI)) + @echo "##### Running gosec #####" + gosec ./... + +.PHONY: govulncheck +govulncheck: + $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI)) + @echo "##### Running govulncheck #####" + govulncheck ./... + +.PHONY: test +test: + @echo "##### Running tests #####" + go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./... diff --git a/vendor/github.com/gorilla/handlers/README.md b/vendor/github.com/gorilla/handlers/README.md index 6eba66bf..02555b26 100644 --- a/vendor/github.com/gorilla/handlers/README.md +++ b/vendor/github.com/gorilla/handlers/README.md @@ -1,9 +1,9 @@ -gorilla/handlers -================ -[![GoDoc](https://godoc.org/github.com/gorilla/handlers?status.svg)](https://godoc.org/github.com/gorilla/handlers) -[![CircleCI](https://circleci.com/gh/gorilla/handlers.svg?style=svg)](https://circleci.com/gh/gorilla/handlers) -[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/handlers/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/handlers?badge) +# gorilla/handlers +![Testing](https://github.com/gorilla/handlers/actions/workflows/test.yml/badge.svg) +[![Codecov](https://codecov.io/github/gorilla/handlers/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/handlers) +[![GoDoc](https://godoc.org/github.com/gorilla/handlers?status.svg)](https://godoc.org/github.com/gorilla/handlers) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/handlers/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/handlers?badge) Package handlers is a collection of handlers (aka "HTTP middleware") for use with Go's `net/http` package (or any framework supporting `http.Handler`), including: diff --git a/vendor/github.com/gorilla/handlers/canonical.go b/vendor/github.com/gorilla/handlers/canonical.go index 8437fefc..7121f530 100644 --- a/vendor/github.com/gorilla/handlers/canonical.go +++ b/vendor/github.com/gorilla/handlers/canonical.go @@ -21,12 +21,11 @@ type canonical struct { // // Example: // -// r := mux.NewRouter() -// canonical := handlers.CanonicalHost("http://www.gorillatoolkit.org", 302) -// r.HandleFunc("/route", YourHandler) -// -// log.Fatal(http.ListenAndServe(":7000", canonical(r))) +// r := mux.NewRouter() +// canonical := handlers.CanonicalHost("http://www.gorillatoolkit.org", 302) +// r.HandleFunc("/route", YourHandler) // +// log.Fatal(http.ListenAndServe(":7000", canonical(r))) func CanonicalHost(domain string, code int) func(h http.Handler) http.Handler { fn := func(h http.Handler) http.Handler { return canonical{h, domain, code} diff --git a/vendor/github.com/gorilla/handlers/compress.go b/vendor/github.com/gorilla/handlers/compress.go index e46a7bfd..d6f58950 100644 --- a/vendor/github.com/gorilla/handlers/compress.go +++ b/vendor/github.com/gorilla/handlers/compress.go @@ -10,47 +10,48 @@ import ( "io" "net/http" "strings" + + "github.com/felixge/httpsnoop" ) +const acceptEncoding string = "Accept-Encoding" + type compressResponseWriter struct { - io.Writer - http.ResponseWriter - http.Hijacker - http.Flusher - http.CloseNotifier + compressor io.Writer + w http.ResponseWriter } -func (w *compressResponseWriter) WriteHeader(c int) { - w.ResponseWriter.Header().Del("Content-Length") - w.ResponseWriter.WriteHeader(c) +func (cw *compressResponseWriter) WriteHeader(c int) { + cw.w.Header().Del("Content-Length") + cw.w.WriteHeader(c) } -func (w *compressResponseWriter) Header() http.Header { - return w.ResponseWriter.Header() -} - -func (w *compressResponseWriter) Write(b []byte) (int, error) { - h := w.ResponseWriter.Header() +func (cw *compressResponseWriter) Write(b []byte) (int, error) { + h := cw.w.Header() if h.Get("Content-Type") == "" { h.Set("Content-Type", http.DetectContentType(b)) } h.Del("Content-Length") - return w.Writer.Write(b) + return cw.compressor.Write(b) +} + +func (cw *compressResponseWriter) ReadFrom(r io.Reader) (int64, error) { + return io.Copy(cw.compressor, r) } type flusher interface { Flush() error } -func (w *compressResponseWriter) Flush() { +func (cw *compressResponseWriter) Flush() { // Flush compressed data if compressor supports it. - if f, ok := w.Writer.(flusher); ok { - f.Flush() + if f, ok := cw.compressor.(flusher); ok { + _ = f.Flush() } // Flush HTTP response. - if w.Flusher != nil { - w.Flusher.Flush() + if f, ok := cw.w.(http.Flusher); ok { + f.Flush() } } @@ -74,77 +75,69 @@ func CompressHandlerLevel(h http.Handler, level int) http.Handler { level = gzip.DefaultCompression } + const ( + gzipEncoding = "gzip" + flateEncoding = "deflate" + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - L: - for _, enc := range strings.Split(r.Header.Get("Accept-Encoding"), ",") { - switch strings.TrimSpace(enc) { - case "gzip": - w.Header().Set("Content-Encoding", "gzip") - r.Header.Del("Accept-Encoding") - w.Header().Add("Vary", "Accept-Encoding") - - gw, _ := gzip.NewWriterLevel(w, level) - defer gw.Close() - - h, hok := w.(http.Hijacker) - if !hok { /* w is not Hijacker... oh well... */ - h = nil - } - - f, fok := w.(http.Flusher) - if !fok { - f = nil - } - - cn, cnok := w.(http.CloseNotifier) - if !cnok { - cn = nil - } - - w = &compressResponseWriter{ - Writer: gw, - ResponseWriter: w, - Hijacker: h, - Flusher: f, - CloseNotifier: cn, - } - - break L - case "deflate": - w.Header().Set("Content-Encoding", "deflate") - r.Header.Del("Accept-Encoding") - w.Header().Add("Vary", "Accept-Encoding") - - fw, _ := flate.NewWriter(w, level) - defer fw.Close() - - h, hok := w.(http.Hijacker) - if !hok { /* w is not Hijacker... oh well... */ - h = nil - } - - f, fok := w.(http.Flusher) - if !fok { - f = nil - } - - cn, cnok := w.(http.CloseNotifier) - if !cnok { - cn = nil - } - - w = &compressResponseWriter{ - Writer: fw, - ResponseWriter: w, - Hijacker: h, - Flusher: f, - CloseNotifier: cn, - } - - break L + // detect what encoding to use + var encoding string + for _, curEnc := range strings.Split(r.Header.Get(acceptEncoding), ",") { + curEnc = strings.TrimSpace(curEnc) + if curEnc == gzipEncoding || curEnc == flateEncoding { + encoding = curEnc + break } } + // always add Accept-Encoding to Vary to prevent intermediate caches corruption + w.Header().Add("Vary", acceptEncoding) + + // if we weren't able to identify an encoding we're familiar with, pass on the + // request to the handler and return + if encoding == "" { + h.ServeHTTP(w, r) + return + } + + if r.Header.Get("Upgrade") != "" { + h.ServeHTTP(w, r) + return + } + + // wrap the ResponseWriter with the writer for the chosen encoding + var encWriter io.WriteCloser + if encoding == gzipEncoding { + encWriter, _ = gzip.NewWriterLevel(w, level) + } else if encoding == flateEncoding { + encWriter, _ = flate.NewWriter(w, level) + } + defer encWriter.Close() + + w.Header().Set("Content-Encoding", encoding) + r.Header.Del(acceptEncoding) + + cw := &compressResponseWriter{ + w: w, + compressor: encWriter, + } + + w = httpsnoop.Wrap(w, httpsnoop.Hooks{ + Write: func(httpsnoop.WriteFunc) httpsnoop.WriteFunc { + return cw.Write + }, + WriteHeader: func(httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc { + return cw.WriteHeader + }, + Flush: func(httpsnoop.FlushFunc) httpsnoop.FlushFunc { + return cw.Flush + }, + ReadFrom: func(rff httpsnoop.ReadFromFunc) httpsnoop.ReadFromFunc { + return cw.ReadFrom + }, + }) + h.ServeHTTP(w, r) }) } diff --git a/vendor/github.com/gorilla/handlers/cors.go b/vendor/github.com/gorilla/handlers/cors.go index 0dcdffb3..8af9c096 100644 --- a/vendor/github.com/gorilla/handlers/cors.go +++ b/vendor/github.com/gorilla/handlers/cors.go @@ -26,14 +26,14 @@ type cors struct { type OriginValidator func(string) bool var ( - defaultCorsOptionStatusCode = 200 - defaultCorsMethods = []string{"GET", "HEAD", "POST"} + defaultCorsOptionStatusCode = http.StatusOK + defaultCorsMethods = []string{http.MethodGet, http.MethodHead, http.MethodPost} defaultCorsHeaders = []string{"Accept", "Accept-Language", "Content-Language", "Origin"} - // (WebKit/Safari v9 sends the Origin header by default in AJAX requests) + // (WebKit/Safari v9 sends the Origin header by default in AJAX requests). ) const ( - corsOptionMethod string = "OPTIONS" + corsOptionMethod string = http.MethodOptions corsAllowOriginHeader string = "Access-Control-Allow-Origin" corsExposeHeadersHeader string = "Access-Control-Expose-Headers" corsMaxAgeHeader string = "Access-Control-Max-Age" @@ -101,10 +101,8 @@ func (ch *cors) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !ch.isMatch(method, defaultCorsMethods) { w.Header().Set(corsAllowMethodsHeader, method) } - } else { - if len(ch.exposedHeaders) > 0 { - w.Header().Set(corsExposeHeadersHeader, strings.Join(ch.exposedHeaders, ",")) - } + } else if len(ch.exposedHeaders) > 0 { + w.Header().Set(corsExposeHeadersHeader, strings.Join(ch.exposedHeaders, ",")) } if ch.allowCredentials { @@ -141,22 +139,21 @@ func (ch *cors) ServeHTTP(w http.ResponseWriter, r *http.Request) { // CORS provides Cross-Origin Resource Sharing middleware. // Example: // -// import ( -// "net/http" +// import ( +// "net/http" // -// "github.com/gorilla/handlers" -// "github.com/gorilla/mux" -// ) +// "github.com/gorilla/handlers" +// "github.com/gorilla/mux" +// ) // -// func main() { -// r := mux.NewRouter() -// r.HandleFunc("/users", UserEndpoint) -// r.HandleFunc("/projects", ProjectEndpoint) -// -// // Apply the CORS middleware to our top-level router, with the defaults. -// http.ListenAndServe(":8000", handlers.CORS()(r)) -// } +// func main() { +// r := mux.NewRouter() +// r.HandleFunc("/users", UserEndpoint) +// r.HandleFunc("/projects", ProjectEndpoint) // +// // Apply the CORS middleware to our top-level router, with the defaults. +// http.ListenAndServe(":8000", handlers.CORS()(r)) +// } func CORS(opts ...CORSOption) func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { ch := parseCORSOptions(opts...) @@ -174,7 +171,7 @@ func parseCORSOptions(opts ...CORSOption) *cors { } for _, option := range opts { - option(ch) + _ = option(ch) //TODO: @bharat-rajani, return error to caller if not nil? } return ch diff --git a/vendor/github.com/gorilla/handlers/handlers.go b/vendor/github.com/gorilla/handlers/handlers.go index d03f2bf1..9b92fce3 100644 --- a/vendor/github.com/gorilla/handlers/handlers.go +++ b/vendor/github.com/gorilla/handlers/handlers.go @@ -35,7 +35,7 @@ func (h MethodHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { } sort.Strings(allow) w.Header().Set("Allow", strings.Join(allow, ", ")) - if req.Method == "OPTIONS" { + if req.Method == http.MethodOptions { w.WriteHeader(http.StatusOK) } else { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) @@ -44,17 +44,13 @@ func (h MethodHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { } // responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP -// status code and body size +// status code and body size. type responseLogger struct { w http.ResponseWriter status int size int } -func (l *responseLogger) Header() http.Header { - return l.w.Header() -} - func (l *responseLogger) Write(b []byte) (int, error) { size, err := l.w.Write(b) l.size += size @@ -74,39 +70,16 @@ func (l *responseLogger) Size() int { return l.size } -func (l *responseLogger) Flush() { - f, ok := l.w.(http.Flusher) - if ok { - f.Flush() - } -} - -type hijackLogger struct { - responseLogger -} - -func (l *hijackLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) { - h := l.responseLogger.w.(http.Hijacker) - conn, rw, err := h.Hijack() - if err == nil && l.responseLogger.status == 0 { +func (l *responseLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) { + conn, rw, err := l.w.(http.Hijacker).Hijack() + if err == nil && l.status == 0 { // The status will be StatusSwitchingProtocols if there was no error and // WriteHeader has not been called yet - l.responseLogger.status = http.StatusSwitchingProtocols + l.status = http.StatusSwitchingProtocols } return conn, rw, err } -type closeNotifyWriter struct { - loggingResponseWriter - http.CloseNotifier -} - -type hijackCloseNotifier struct { - loggingResponseWriter - http.Hijacker - http.CloseNotifier -} - // isContentType validates the Content-Type header matches the supplied // contentType. That is, its type and subtype match. func isContentType(h http.Header, contentType string) bool { @@ -124,7 +97,7 @@ func isContentType(h http.Header, contentType string) bool { // Only PUT, POST, and PATCH requests are considered. func ContentTypeHandler(h http.Handler, contentTypes ...string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !(r.Method == "PUT" || r.Method == "POST" || r.Method == "PATCH") { + if !(r.Method == http.MethodPut || r.Method == http.MethodPost || r.Method == http.MethodPatch) { h.ServeHTTP(w, r) return } @@ -135,7 +108,10 @@ func ContentTypeHandler(h http.Handler, contentTypes ...string) http.Handler { return } } - http.Error(w, fmt.Sprintf("Unsupported content type %q; expected one of %q", r.Header.Get("Content-Type"), contentTypes), http.StatusUnsupportedMediaType) + http.Error(w, fmt.Sprintf("Unsupported content type %q; expected one of %q", + r.Header.Get("Content-Type"), + contentTypes), + http.StatusUnsupportedMediaType) }) } @@ -160,12 +136,12 @@ const ( // Form method takes precedence over header method. func HTTPMethodOverrideHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { + if r.Method == http.MethodPost { om := r.FormValue(HTTPMethodOverrideFormKey) if om == "" { om = r.Header.Get(HTTPMethodOverrideHeader) } - if om == "PUT" || om == "PATCH" || om == "DELETE" { + if om == http.MethodPut || om == http.MethodPatch || om == http.MethodDelete { r.Method = om } } diff --git a/vendor/github.com/gorilla/handlers/handlers_go18.go b/vendor/github.com/gorilla/handlers/handlers_go18.go deleted file mode 100644 index 40f69146..00000000 --- a/vendor/github.com/gorilla/handlers/handlers_go18.go +++ /dev/null @@ -1,29 +0,0 @@ -// +build go1.8 - -package handlers - -import ( - "fmt" - "net/http" -) - -type loggingResponseWriter interface { - commonLoggingResponseWriter - http.Pusher -} - -func (l *responseLogger) Push(target string, opts *http.PushOptions) error { - p, ok := l.w.(http.Pusher) - if !ok { - return fmt.Errorf("responseLogger does not implement http.Pusher") - } - return p.Push(target, opts) -} - -func (c *compressResponseWriter) Push(target string, opts *http.PushOptions) error { - p, ok := c.ResponseWriter.(http.Pusher) - if !ok { - return fmt.Errorf("compressResponseWriter does not implement http.Pusher") - } - return p.Push(target, opts) -} diff --git a/vendor/github.com/gorilla/handlers/handlers_pre18.go b/vendor/github.com/gorilla/handlers/handlers_pre18.go deleted file mode 100644 index 197836ab..00000000 --- a/vendor/github.com/gorilla/handlers/handlers_pre18.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build !go1.8 - -package handlers - -type loggingResponseWriter interface { - commonLoggingResponseWriter -} diff --git a/vendor/github.com/gorilla/handlers/logging.go b/vendor/github.com/gorilla/handlers/logging.go index 88c25e72..2badb6fb 100644 --- a/vendor/github.com/gorilla/handlers/logging.go +++ b/vendor/github.com/gorilla/handlers/logging.go @@ -12,11 +12,13 @@ import ( "strconv" "time" "unicode/utf8" + + "github.com/felixge/httpsnoop" ) // Logging -// LogFormatterParams is the structure any formatter will be handed when time to log comes +// LogFormatterParams is the structure any formatter will be handed when time to log comes. type LogFormatterParams struct { Request *http.Request URL url.URL @@ -25,7 +27,7 @@ type LogFormatterParams struct { Size int } -// LogFormatter gives the signature of the formatter function passed to CustomLoggingHandler +// LogFormatter gives the signature of the formatter function passed to CustomLoggingHandler. type LogFormatter func(writer io.Writer, params LogFormatterParams) // loggingHandler is the http.Handler implementation for LoggingHandlerTo and its @@ -39,12 +41,15 @@ type loggingHandler struct { func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { t := time.Now() - logger := makeLogger(w) + logger, w := makeLogger(w) url := *req.URL - h.handler.ServeHTTP(logger, req) + h.handler.ServeHTTP(w, req) if req.MultipartForm != nil { - req.MultipartForm.RemoveAll() + err := req.MultipartForm.RemoveAll() + if err != nil { + return + } } params := LogFormatterParams{ @@ -58,34 +63,23 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { h.formatter(h.writer, params) } -func makeLogger(w http.ResponseWriter) loggingResponseWriter { - var logger loggingResponseWriter = &responseLogger{w: w, status: http.StatusOK} - if _, ok := w.(http.Hijacker); ok { - logger = &hijackLogger{responseLogger{w: w, status: http.StatusOK}} - } - h, ok1 := logger.(http.Hijacker) - c, ok2 := w.(http.CloseNotifier) - if ok1 && ok2 { - return hijackCloseNotifier{logger, h, c} - } - if ok2 { - return &closeNotifyWriter{logger, c} - } - return logger -} - -type commonLoggingResponseWriter interface { - http.ResponseWriter - http.Flusher - Status() int - Size() int +func makeLogger(w http.ResponseWriter) (*responseLogger, http.ResponseWriter) { + logger := &responseLogger{w: w, status: http.StatusOK} + return logger, httpsnoop.Wrap(w, httpsnoop.Hooks{ + Write: func(httpsnoop.WriteFunc) httpsnoop.WriteFunc { + return logger.Write + }, + WriteHeader: func(httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc { + return logger.WriteHeader + }, + }) } const lowerhex = "0123456789abcdef" func appendQuoted(buf []byte, s string) []byte { var runeTmp [utf8.UTFMax]byte - for width := 0; len(s) > 0; s = s[width:] { + for width := 0; len(s) > 0; s = s[width:] { //nolint: wastedassign //TODO: why width starts from 0and reassigned as 1 r := rune(s[0]) width = 1 if r >= utf8.RuneSelf { @@ -145,7 +139,6 @@ func appendQuoted(buf []byte, s string) []byte { } } return buf - } // buildCommonLogLine builds a log entry for req in Apache Common Log Format. @@ -160,7 +153,6 @@ func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int } host, _, err := net.SplitHostPort(req.RemoteAddr) - if err != nil { host = req.RemoteAddr } @@ -202,7 +194,7 @@ func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int func writeLog(writer io.Writer, params LogFormatterParams) { buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size) buf = append(buf, '\n') - writer.Write(buf) + _, _ = writer.Write(buf) } // writeCombinedLog writes a log entry for req to w in Apache Combined Log Format. @@ -215,7 +207,7 @@ func writeCombinedLog(writer io.Writer, params LogFormatterParams) { buf = append(buf, `" "`...) buf = appendQuoted(buf, params.Request.UserAgent()) buf = append(buf, '"', '\n') - writer.Write(buf) + _, _ = writer.Write(buf) } // CombinedLoggingHandler return a http.Handler that wraps h and logs requests to out in @@ -223,7 +215,7 @@ func writeCombinedLog(writer io.Writer, params LogFormatterParams) { // // See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format. // -// LoggingHandler always sets the ident field of the log to - +// LoggingHandler always sets the ident field of the log to -. func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler { return loggingHandler{out, h, writeCombinedLog} } @@ -237,19 +229,18 @@ func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler { // // Example: // -// r := mux.NewRouter() -// r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { -// w.Write([]byte("This is a catch-all route")) -// }) -// loggedRouter := handlers.LoggingHandler(os.Stdout, r) -// http.ListenAndServe(":1123", loggedRouter) -// +// r := mux.NewRouter() +// r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("This is a catch-all route")) +// }) +// loggedRouter := handlers.LoggingHandler(os.Stdout, r) +// http.ListenAndServe(":1123", loggedRouter) func LoggingHandler(out io.Writer, h http.Handler) http.Handler { return loggingHandler{out, h, writeLog} } // CustomLoggingHandler provides a way to supply a custom log formatter -// while taking advantage of the mechanisms in this package +// while taking advantage of the mechanisms in this package. func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler { return loggingHandler{out, h, f} } diff --git a/vendor/github.com/gorilla/handlers/proxy_headers.go b/vendor/github.com/gorilla/handlers/proxy_headers.go index ed939dce..281d753e 100644 --- a/vendor/github.com/gorilla/handlers/proxy_headers.go +++ b/vendor/github.com/gorilla/handlers/proxy_headers.go @@ -18,7 +18,7 @@ var ( var ( // RFC7239 defines a new "Forwarded: " header designed to replace the // existing use of X-Forwarded-* headers. - // e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43 + // e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43. forwarded = http.CanonicalHeaderKey("Forwarded") // Allows for a sub-match of the first value after 'for=' to the next // comma, semi-colon or space. The match is case-insensitive. @@ -67,7 +67,9 @@ func ProxyHeaders(h http.Handler) http.Handler { func getIP(r *http.Request) string { var addr string - if fwd := r.Header.Get(xForwardedFor); fwd != "" { + switch { + case r.Header.Get(xForwardedFor) != "": + fwd := r.Header.Get(xForwardedFor) // Only grab the first (client) address. Note that '192.168.0.1, // 10.1.1.1' is a valid key for X-Forwarded-For where addresses after // the first may represent forwarding proxies earlier in the chain. @@ -76,17 +78,15 @@ func getIP(r *http.Request) string { s = len(fwd) } addr = fwd[:s] - } else if fwd := r.Header.Get(xRealIP); fwd != "" { - // X-Real-IP should only contain one IP address (the client making the - // request). - addr = fwd - } else if fwd := r.Header.Get(forwarded); fwd != "" { + case r.Header.Get(xRealIP) != "": + addr = r.Header.Get(xRealIP) + case r.Header.Get(forwarded) != "": // match should contain at least two elements if the protocol was // specified in the Forwarded header. The first element will always be // the 'for=' capture, which we ignore. In the case of multiple IP // addresses (for=8.8.8.8, 8.8.4.4,172.16.1.20 is valid) we only // extract the first, which should be the client IP. - if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 { + if match := forRegex.FindStringSubmatch(r.Header.Get(forwarded)); len(match) > 1 { // IPv6 addresses in Forwarded headers are quoted-strings. We strip // these quotes. addr = strings.Trim(match[1], `"`) diff --git a/vendor/github.com/gorilla/handlers/recovery.go b/vendor/github.com/gorilla/handlers/recovery.go index b1be9dc8..0d4f955e 100644 --- a/vendor/github.com/gorilla/handlers/recovery.go +++ b/vendor/github.com/gorilla/handlers/recovery.go @@ -19,7 +19,7 @@ type recoveryHandler struct { // RecoveryOption provides a functional approach to define // configuration for a handler; such as setting the logging -// whether or not to print strack traces on panic. +// whether or not to print stack traces on panic. type RecoveryOption func(http.Handler) func parseRecoveryOptions(h http.Handler, opts ...RecoveryOption) http.Handler { @@ -36,12 +36,12 @@ func parseRecoveryOptions(h http.Handler, opts ...RecoveryOption) http.Handler { // // Example: // -// r := mux.NewRouter() -// r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { -// panic("Unexpected error!") -// }) +// r := mux.NewRouter() +// r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { +// panic("Unexpected error!") +// }) // -// http.ListenAndServe(":1123", handlers.RecoveryHandler()(r)) +// http.ListenAndServe(":1123", handlers.RecoveryHandler()(r)) func RecoveryHandler(opts ...RecoveryOption) func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler { r := &recoveryHandler{handler: h} @@ -50,20 +50,22 @@ func RecoveryHandler(opts ...RecoveryOption) func(h http.Handler) http.Handler { } // RecoveryLogger is a functional option to override -// the default logger +// the default logger. func RecoveryLogger(logger RecoveryHandlerLogger) RecoveryOption { return func(h http.Handler) { - r := h.(*recoveryHandler) + r := h.(*recoveryHandler) //nolint:errcheck //TODO: + // @bharat-rajani should return type-assertion error but would break the API? r.logger = logger } } // PrintRecoveryStack is a functional option to enable // or disable printing stack traces on panic. -func PrintRecoveryStack(print bool) RecoveryOption { +func PrintRecoveryStack(shouldPrint bool) RecoveryOption { return func(h http.Handler) { - r := h.(*recoveryHandler) - r.printStack = print + r := h.(*recoveryHandler) //nolint:errcheck //TODO: + // @bharat-rajani should return type-assertion error but would break the API? + r.printStack = shouldPrint } } @@ -86,6 +88,11 @@ func (h recoveryHandler) log(v ...interface{}) { } if h.printStack { - debug.PrintStack() + stack := string(debug.Stack()) + if h.logger != nil { + h.logger.Println(stack) + } else { + log.Println(stack) + } } } diff --git a/vendor/github.com/gorilla/mux/.editorconfig b/vendor/github.com/gorilla/mux/.editorconfig new file mode 100644 index 00000000..c6b74c3e --- /dev/null +++ b/vendor/github.com/gorilla/mux/.editorconfig @@ -0,0 +1,20 @@ +; https://editorconfig.org/ + +root = true + +[*] +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[{Makefile,go.mod,go.sum,*.go,.gitmodules}] +indent_style = tab +indent_size = 4 + +[*.md] +indent_size = 4 +trim_trailing_whitespace = false + +eclint_indent_style = unset \ No newline at end of file diff --git a/vendor/github.com/gorilla/mux/.gitignore b/vendor/github.com/gorilla/mux/.gitignore new file mode 100644 index 00000000..84039fec --- /dev/null +++ b/vendor/github.com/gorilla/mux/.gitignore @@ -0,0 +1 @@ +coverage.coverprofile diff --git a/vendor/github.com/gorilla/mux/AUTHORS b/vendor/github.com/gorilla/mux/AUTHORS deleted file mode 100644 index b722392e..00000000 --- a/vendor/github.com/gorilla/mux/AUTHORS +++ /dev/null @@ -1,8 +0,0 @@ -# This is the official list of gorilla/mux authors for copyright purposes. -# -# Please keep the list sorted. - -Google LLC (https://opensource.google.com/) -Kamil Kisielk -Matt Silverlock -Rodrigo Moraes (https://github.com/moraes) diff --git a/vendor/github.com/gorilla/mux/LICENSE b/vendor/github.com/gorilla/mux/LICENSE index 6903df63..bb9d80bc 100644 --- a/vendor/github.com/gorilla/mux/LICENSE +++ b/vendor/github.com/gorilla/mux/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved. +Copyright (c) 2023 The Gorilla Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/vendor/github.com/gorilla/mux/Makefile b/vendor/github.com/gorilla/mux/Makefile new file mode 100644 index 00000000..98f5ab75 --- /dev/null +++ b/vendor/github.com/gorilla/mux/Makefile @@ -0,0 +1,34 @@ +GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '') +GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +GO_SEC=$(shell which gosec 2> /dev/null || echo '') +GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest + +GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '') +GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest + +.PHONY: golangci-lint +golangci-lint: + $(if $(GO_LINT), ,go install $(GO_LINT_URI)) + @echo "##### Running golangci-lint" + golangci-lint run -v + +.PHONY: gosec +gosec: + $(if $(GO_SEC), ,go install $(GO_SEC_URI)) + @echo "##### Running gosec" + gosec ./... + +.PHONY: govulncheck +govulncheck: + $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI)) + @echo "##### Running govulncheck" + govulncheck ./... + +.PHONY: verify +verify: golangci-lint gosec govulncheck + +.PHONY: test +test: + @echo "##### Running tests" + go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./... \ No newline at end of file diff --git a/vendor/github.com/gorilla/mux/README.md b/vendor/github.com/gorilla/mux/README.md index 35eea9f1..382513d5 100644 --- a/vendor/github.com/gorilla/mux/README.md +++ b/vendor/github.com/gorilla/mux/README.md @@ -1,12 +1,12 @@ # gorilla/mux -[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) -[![CircleCI](https://circleci.com/gh/gorilla/mux.svg?style=svg)](https://circleci.com/gh/gorilla/mux) -[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) +![testing](https://github.com/gorilla/mux/actions/workflows/test.yml/badge.svg) +[![codecov](https://codecov.io/github/gorilla/mux/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/mux) +[![godoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) +[![sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) -![Gorilla Logo](https://cloud-cdn.questionable.services/gorilla-icon-64.png) -https://www.gorillatoolkit.org/pkg/mux +![Gorilla Logo](https://github.com/gorilla/.github/assets/53367916/d92caabf-98e0-473e-bfbf-ab554ba435e5) Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to their respective handler. @@ -247,32 +247,25 @@ type spaHandler struct { // file located at the index path on the SPA handler will be served. This // is suitable behavior for serving an SPA (single page application). func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // get the absolute path to prevent directory traversal - path, err := filepath.Abs(r.URL.Path) - if err != nil { - // if we failed to get the absolute path respond with a 400 bad request - // and stop - http.Error(w, err.Error(), http.StatusBadRequest) - return - } + // Join internally call path.Clean to prevent directory traversal + path := filepath.Join(h.staticPath, r.URL.Path) - // prepend the path with the path to the static directory - path = filepath.Join(h.staticPath, path) - - // check whether a file exists at the given path - _, err = os.Stat(path) - if os.IsNotExist(err) { - // file does not exist, serve index.html + // check whether a file exists or is a directory at the given path + fi, err := os.Stat(path) + if os.IsNotExist(err) || fi.IsDir() { + // file does not exist or path is a directory, serve index.html http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) return - } else if err != nil { - // if we got an error (that wasn't that the file doesn't exist) stating the - // file, return a 500 internal server error and stop - http.Error(w, err.Error(), http.StatusInternalServerError) - return } - // otherwise, use http.FileServer to serve the static dir + if err != nil { + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // otherwise, use http.FileServer to serve the static file http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) } @@ -375,6 +368,19 @@ url, err := r.Get("article").URL("subdomain", "news", "id", "42") ``` +To find all the required variables for a given route when calling `URL()`, the method `GetVarNames()` is available: +```go +r := mux.NewRouter() +r.Host("{domain}"). + Path("/{group}/{item_id}"). + Queries("some_data1", "{some_data1}"). + Queries("some_data2", "{some_data2}"). + Name("article") + +// Will print [domain group item_id some_data1 some_data2] +fmt.Println(r.Get("article").GetVarNames()) + +``` ### Walking Routes The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example, @@ -572,7 +578,7 @@ func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler r := mux.NewRouter() r.HandleFunc("/", handler) -amw := authenticationMiddleware{} +amw := authenticationMiddleware{tokenUsers: make(map[string]string)} amw.Populate() r.Use(amw.Middleware) @@ -758,7 +764,8 @@ func TestMetricsHandler(t *testing.T) { rr := httptest.NewRecorder() - // Need to create a router that we can pass the request through so that the vars will be added to the context + // To add the vars to the context, + // we need to create a router through which we can pass the request. router := mux.NewRouter() router.HandleFunc("/metrics/{type}", MetricsHandler) router.ServeHTTP(rr, req) diff --git a/vendor/github.com/gorilla/mux/doc.go b/vendor/github.com/gorilla/mux/doc.go index bd5a38b5..80601351 100644 --- a/vendor/github.com/gorilla/mux/doc.go +++ b/vendor/github.com/gorilla/mux/doc.go @@ -10,18 +10,18 @@ http.ServeMux, mux.Router matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: - * Requests can be matched based on URL host, path, path prefix, schemes, - header and query values, HTTP methods or using custom matchers. - * URL hosts, paths and query values can have variables with an optional - regular expression. - * Registered URLs can be built, or "reversed", which helps maintaining - references to resources. - * Routes can be used as subrouters: nested routes are only tested if the - parent route matches. This is useful to define groups of routes that - share common conditions like a host, a path prefix or other repeated - attributes. As a bonus, this optimizes request matching. - * It implements the http.Handler interface so it is compatible with the - standard http.ServeMux. + - Requests can be matched based on URL host, path, path prefix, schemes, + header and query values, HTTP methods or using custom matchers. + - URL hosts, paths and query values can have variables with an optional + regular expression. + - Registered URLs can be built, or "reversed", which helps maintaining + references to resources. + - Routes can be used as subrouters: nested routes are only tested if the + parent route matches. This is useful to define groups of routes that + share common conditions like a host, a path prefix or other repeated + attributes. As a bonus, this optimizes request matching. + - It implements the http.Handler interface so it is compatible with the + standard http.ServeMux. Let's start registering a couple of URL paths and handlers: @@ -301,6 +301,5 @@ A more complex authentication middleware, which maps session token to users, cou r.Use(amw.Middleware) Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. - */ package mux diff --git a/vendor/github.com/gorilla/mux/mux.go b/vendor/github.com/gorilla/mux/mux.go index c9ba6470..1e089906 100644 --- a/vendor/github.com/gorilla/mux/mux.go +++ b/vendor/github.com/gorilla/mux/mux.go @@ -31,24 +31,26 @@ func NewRouter() *Router { // It implements the http.Handler interface, so it can be registered to serve // requests: // -// var router = mux.NewRouter() +// var router = mux.NewRouter() // -// func main() { -// http.Handle("/", router) -// } +// func main() { +// http.Handle("/", router) +// } // // Or, for Google App Engine, register it in a init() function: // -// func init() { -// http.Handle("/", router) -// } +// func init() { +// http.Handle("/", router) +// } // // This will send all incoming requests to the router. type Router struct { // Configurable Handler to be used when no route matches. + // This can be used to render your own 404 Not Found errors. NotFoundHandler http.Handler // Configurable Handler to be used when the request method does not match the route. + // This can be used to render your own 405 Method Not Allowed errors. MethodNotAllowedHandler http.Handler // Routes to be matched, in order. @@ -435,8 +437,7 @@ func Vars(r *http.Request) map[string]string { // CurrentRoute returns the matched route for the current request, if any. // This only works when called inside the handler of the matched route // because the matched route is stored in the request context which is cleared -// after the handler returns, unless the KeepContext option is set on the -// Router. +// after the handler returns. func CurrentRoute(r *http.Request) *Route { if rv := r.Context().Value(routeKey); rv != nil { return rv.(*Route) diff --git a/vendor/github.com/gorilla/mux/regexp.go b/vendor/github.com/gorilla/mux/regexp.go index 96dd94ad..5d05cfa0 100644 --- a/vendor/github.com/gorilla/mux/regexp.go +++ b/vendor/github.com/gorilla/mux/regexp.go @@ -22,10 +22,10 @@ type routeRegexpOptions struct { type regexpType int const ( - regexpTypePath regexpType = 0 - regexpTypeHost regexpType = 1 - regexpTypePrefix regexpType = 2 - regexpTypeQuery regexpType = 3 + regexpTypePath regexpType = iota + regexpTypeHost + regexpTypePrefix + regexpTypeQuery ) // newRouteRegexp parses a route template and returns a routeRegexp, @@ -195,7 +195,7 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { // url builds a URL part using the given values. func (r *routeRegexp) url(values map[string]string) (string, error) { - urlValues := make([]interface{}, len(r.varsN), len(r.varsN)) + urlValues := make([]interface{}, len(r.varsN)) for k, v := range r.varsN { value, ok := values[v] if !ok { @@ -325,6 +325,12 @@ func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { // Store host variables. if v.host != nil { host := getHost(req) + if v.host.wildcardHostPort { + // Don't be strict on the port match + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + } matches := v.host.regexp.FindStringSubmatchIndex(host) if len(matches) > 0 { extractVars(host, matches, v.host.varsN, m.Vars) diff --git a/vendor/github.com/gorilla/mux/route.go b/vendor/github.com/gorilla/mux/route.go index 750afe57..e8f11df2 100644 --- a/vendor/github.com/gorilla/mux/route.go +++ b/vendor/github.com/gorilla/mux/route.go @@ -64,8 +64,18 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool { match.MatchErr = nil } - matchErr = nil + matchErr = nil // nolint:ineffassign return false + } else { + // Multiple routes may share the same path but use different HTTP methods. For instance: + // Route 1: POST "/users/{id}". + // Route 2: GET "/users/{id}", parameters: "id": "[0-9]+". + // + // The router must handle these cases correctly. For a GET request to "/users/abc" with "id" as "-2", + // The router should return a "Not Found" error as no route fully matches this request. + if match.MatchErr == ErrMethodMismatch { + match.MatchErr = nil + } } } @@ -230,9 +240,9 @@ func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { // Headers adds a matcher for request header values. // It accepts a sequence of key/value pairs to be matched. For example: // -// r := mux.NewRouter() -// r.Headers("Content-Type", "application/json", -// "X-Requested-With", "XMLHttpRequest") +// r := mux.NewRouter().NewRoute() +// r.Headers("Content-Type", "application/json", +// "X-Requested-With", "XMLHttpRequest") // // The above route will only match if both request header values match. // If the value is an empty string, it will match any value if the key is set. @@ -255,9 +265,9 @@ func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool { // HeadersRegexp accepts a sequence of key/value pairs, where the value has regex // support. For example: // -// r := mux.NewRouter() -// r.HeadersRegexp("Content-Type", "application/(text|json)", -// "X-Requested-With", "XMLHttpRequest") +// r := mux.NewRouter().NewRoute() +// r.HeadersRegexp("Content-Type", "application/(text|json)", +// "X-Requested-With", "XMLHttpRequest") // // The above route will only match if both the request header matches both regular expressions. // If the value is an empty string, it will match any value if the key is set. @@ -283,10 +293,10 @@ func (r *Route) HeadersRegexp(pairs ...string) *Route { // // For example: // -// r := mux.NewRouter() -// r.Host("www.example.com") -// r.Host("{subdomain}.domain.com") -// r.Host("{subdomain:[a-z]+}.domain.com") +// r := mux.NewRouter().NewRoute() +// r.Host("www.example.com") +// r.Host("{subdomain}.domain.com") +// r.Host("{subdomain:[a-z]+}.domain.com") // // Variable names must be unique in a given route. They can be retrieved // calling mux.Vars(request). @@ -342,11 +352,11 @@ func (r *Route) Methods(methods ...string) *Route { // // For example: // -// r := mux.NewRouter() -// r.Path("/products/").Handler(ProductsHandler) -// r.Path("/products/{key}").Handler(ProductsHandler) -// r.Path("/articles/{category}/{id:[0-9]+}"). -// Handler(ArticleHandler) +// r := mux.NewRouter().NewRoute() +// r.Path("/products/").Handler(ProductsHandler) +// r.Path("/products/{key}").Handler(ProductsHandler) +// r.Path("/articles/{category}/{id:[0-9]+}"). +// Handler(ArticleHandler) // // Variable names must be unique in a given route. They can be retrieved // calling mux.Vars(request). @@ -377,8 +387,8 @@ func (r *Route) PathPrefix(tpl string) *Route { // It accepts a sequence of key/value pairs. Values may define variables. // For example: // -// r := mux.NewRouter() -// r.Queries("foo", "bar", "id", "{id:[0-9]+}") +// r := mux.NewRouter().NewRoute() +// r.Queries("foo", "bar", "id", "{id:[0-9]+}") // // The above route will only match if the URL contains the defined queries // values, e.g.: ?foo=bar&id=42. @@ -473,11 +483,11 @@ func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { // // It will test the inner routes only if the parent route matched. For example: // -// r := mux.NewRouter() -// s := r.Host("www.example.com").Subrouter() -// s.HandleFunc("/products/", ProductsHandler) -// s.HandleFunc("/products/{key}", ProductHandler) -// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) +// r := mux.NewRouter().NewRoute() +// s := r.Host("www.example.com").Subrouter() +// s.HandleFunc("/products/", ProductsHandler) +// s.HandleFunc("/products/{key}", ProductHandler) +// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) // // Here, the routes registered in the subrouter won't be tested if the host // doesn't match. @@ -497,36 +507,36 @@ func (r *Route) Subrouter() *Router { // It accepts a sequence of key/value pairs for the route variables. For // example, given this route: // -// r := mux.NewRouter() -// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). -// Name("article") +// r := mux.NewRouter() +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Name("article") // // ...a URL for it can be built using: // -// url, err := r.Get("article").URL("category", "technology", "id", "42") +// url, err := r.Get("article").URL("category", "technology", "id", "42") // // ...which will return an url.URL with the following path: // -// "/articles/technology/42" +// "/articles/technology/42" // // This also works for host variables: // -// r := mux.NewRouter() -// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). -// Host("{subdomain}.domain.com"). -// Name("article") +// r := mux.NewRouter() +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Host("{subdomain}.domain.com"). +// Name("article") // -// // url.String() will be "http://news.domain.com/articles/technology/42" -// url, err := r.Get("article").URL("subdomain", "news", -// "category", "technology", -// "id", "42") +// // url.String() will be "http://news.domain.com/articles/technology/42" +// url, err := r.Get("article").URL("subdomain", "news", +// "category", "technology", +// "id", "42") // // The scheme of the resulting url will be the first argument that was passed to Schemes: // -// // url.String() will be "https://example.com" -// r := mux.NewRouter() -// url, err := r.Host("example.com") -// .Schemes("https", "http").URL() +// // url.String() will be "https://example.com" +// r := mux.NewRouter().NewRoute() +// url, err := r.Host("example.com") +// .Schemes("https", "http").URL() // // All variables defined in the route are required, and their values must // conform to the corresponding patterns. @@ -718,6 +728,25 @@ func (r *Route) GetHostTemplate() (string, error) { return r.regexp.host.template, nil } +// GetVarNames returns the names of all variables added by regexp matchers +// These can be used to know which route variables should be passed into r.URL() +func (r *Route) GetVarNames() ([]string, error) { + if r.err != nil { + return nil, r.err + } + var varNames []string + if r.regexp.host != nil { + varNames = append(varNames, r.regexp.host.varsN...) + } + if r.regexp.path != nil { + varNames = append(varNames, r.regexp.path.varsN...) + } + for _, regx := range r.regexp.queries { + varNames = append(varNames, regx.varsN...) + } + return varNames, nil +} + // prepareVars converts the route variable pairs into a map. If the route has a // BuildVarsFunc, it is invoked. func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { diff --git a/vendor/github.com/jmoiron/sqlx/.gitignore b/vendor/github.com/jmoiron/sqlx/.gitignore index 529841cf..b2be23c8 100644 --- a/vendor/github.com/jmoiron/sqlx/.gitignore +++ b/vendor/github.com/jmoiron/sqlx/.gitignore @@ -6,6 +6,7 @@ # Folders _obj _test +.idea # Architecture specific extensions/prefixes *.[568vq] diff --git a/vendor/github.com/jmoiron/sqlx/.travis.yml b/vendor/github.com/jmoiron/sqlx/.travis.yml index 6bc68d67..1cfa28cb 100644 --- a/vendor/github.com/jmoiron/sqlx/.travis.yml +++ b/vendor/github.com/jmoiron/sqlx/.travis.yml @@ -18,9 +18,8 @@ before_install: # go versions to test go: - - "1.8" - - "1.9" - - "1.10.x" + - "1.15.x" + - "1.16.x" # run tests w/ coverage script: diff --git a/vendor/github.com/jmoiron/sqlx/README.md b/vendor/github.com/jmoiron/sqlx/README.md index 83903436..0d715929 100644 --- a/vendor/github.com/jmoiron/sqlx/README.md +++ b/vendor/github.com/jmoiron/sqlx/README.md @@ -15,30 +15,34 @@ Major additional concepts are: * `Get` and `Select` to go quickly from query to struct/slice In addition to the [godoc API documentation](http://godoc.org/github.com/jmoiron/sqlx), -there is also some [standard documentation](http://jmoiron.github.io/sqlx/) that +there is also some [user documentation](http://jmoiron.github.io/sqlx/) that explains how to use `database/sql` along with sqlx. ## Recent Changes -* The [introduction](https://github.com/jmoiron/sqlx/pull/387) of `sql.ColumnType` sets the required minimum Go version to 1.8. +1.3.0: -* sqlx/types.JsonText has been renamed to JSONText to follow Go naming conventions. +* `sqlx.DB.Connx(context.Context) *sqlx.Conn` +* `sqlx.BindDriver(driverName, bindType)` +* support for `[]map[string]interface{}` to do "batch" insertions +* allocation & perf improvements for `sqlx.In` -This breaks backwards compatibility, but it's in a way that is trivially fixable -(`s/JsonText/JSONText/g`). The `types` package is both experimental and not in -active development currently. +DB.Connx returns an `sqlx.Conn`, which is an `sql.Conn`-alike consistent with +sqlx's wrapping of other types. -* Using Go 1.6 and below with `types.JSONText` and `types.GzippedText` can be _potentially unsafe_, **especially** when used with common auto-scan sqlx idioms like `Select` and `Get`. See [golang bug #13905](https://github.com/golang/go/issues/13905). +`BindDriver` allows users to control the bindvars that sqlx will use for drivers, +and add new drivers at runtime. This results in a very slight performance hit +when resolving the driver into a bind type (~40ns per call), but it allows users +to specify what bindtype their driver uses even when sqlx has not been updated +to know about it by default. ### Backwards Compatibility -There is no Go1-like promise of absolute stability, but I take the issue seriously -and will maintain the library in a compatible state unless vital bugs prevent me -from doing so. Since [#59](https://github.com/jmoiron/sqlx/issues/59) and -[#60](https://github.com/jmoiron/sqlx/issues/60) necessitated breaking behavior, -a wider API cleanup was done at the time of fixing. It's possible this will happen -in future; if it does, a git tag will be provided for users requiring the old -behavior to continue to use it until such a time as they can migrate. +Compatibility with the most recent two versions of Go is a requirement for any +new changes. Compatibility beyond that is not guaranteed. + +Versioning is done with Go modules. Breaking changes (eg. removing deprecated API) +will get major version number bumps. ## install @@ -102,7 +106,7 @@ type Place struct { } func main() { - // this Pings the database trying to connect, panics on error + // this Pings the database trying to connect // use sqlx.Open() for sql.Open() semantics db, err := sqlx.Connect("postgres", "user=foo dbname=bar sslmode=disable") if err != nil { @@ -182,6 +186,28 @@ func main() { // as the name -> db mapping, so struct fields are lowercased and the `db` tag // is taken into consideration. rows, err = db.NamedQuery(`SELECT * FROM person WHERE first_name=:first_name`, jason) + + + // batch insert + + // batch insert with structs + personStructs := []Person{ + {FirstName: "Ardie", LastName: "Savea", Email: "asavea@ab.co.nz"}, + {FirstName: "Sonny Bill", LastName: "Williams", Email: "sbw@ab.co.nz"}, + {FirstName: "Ngani", LastName: "Laumape", Email: "nlaumape@ab.co.nz"}, + } + + _, err = db.NamedExec(`INSERT INTO person (first_name, last_name, email) + VALUES (:first_name, :last_name, :email)`, personStructs) + + // batch insert with maps + personMaps := []map[string]interface{}{ + {"first_name": "Ardie", "last_name": "Savea", "email": "asavea@ab.co.nz"}, + {"first_name": "Sonny Bill", "last_name": "Williams", "email": "sbw@ab.co.nz"}, + {"first_name": "Ngani", "last_name": "Laumape", "email": "nlaumape@ab.co.nz"}, + } + + _, err = db.NamedExec(`INSERT INTO person (first_name, last_name, email) + VALUES (:first_name, :last_name, :email)`, personMaps) } ``` - diff --git a/vendor/github.com/jmoiron/sqlx/bind.go b/vendor/github.com/jmoiron/sqlx/bind.go index 0a48252a..ec0da4e7 100644 --- a/vendor/github.com/jmoiron/sqlx/bind.go +++ b/vendor/github.com/jmoiron/sqlx/bind.go @@ -7,6 +7,7 @@ import ( "reflect" "strconv" "strings" + "sync" "github.com/jmoiron/sqlx/reflectx" ) @@ -20,21 +21,36 @@ const ( AT ) +var defaultBinds = map[int][]string{ + DOLLAR: []string{"postgres", "pgx", "pq-timeouts", "cloudsqlpostgres", "ql", "nrpostgres", "cockroach"}, + QUESTION: []string{"mysql", "sqlite3", "nrmysql", "nrsqlite3"}, + NAMED: []string{"oci8", "ora", "goracle", "godror"}, + AT: []string{"sqlserver"}, +} + +var binds sync.Map + +func init() { + for bind, drivers := range defaultBinds { + for _, driver := range drivers { + BindDriver(driver, bind) + } + } + +} + // BindType returns the bindtype for a given database given a drivername. func BindType(driverName string) int { - switch driverName { - case "postgres", "pgx", "pq-timeouts", "cloudsqlpostgres": - return DOLLAR - case "mysql": - return QUESTION - case "sqlite3": - return QUESTION - case "oci8", "ora", "goracle": - return NAMED - case "sqlserver": - return AT + itype, ok := binds.Load(driverName) + if !ok { + return UNKNOWN } - return UNKNOWN + return itype.(int) +} + +// BindDriver sets the BindType for driverName to bindType. +func BindDriver(driverName string, bindType int) { + binds.Store(driverName, bindType) } // FIXME: this should be able to be tolerant of escaped ?'s in queries without @@ -98,6 +114,28 @@ func rebindBuff(bindType int, query string) string { return rqb.String() } +func asSliceForIn(i interface{}) (v reflect.Value, ok bool) { + if i == nil { + return reflect.Value{}, false + } + + v = reflect.ValueOf(i) + t := reflectx.Deref(v.Type()) + + // Only expand slices + if t.Kind() != reflect.Slice { + return reflect.Value{}, false + } + + // []byte is a driver.Value type so it should not be expanded + if t == reflect.TypeOf([]byte{}) { + return reflect.Value{}, false + + } + + return v, true +} + // In expands slice values in args, returning the modified query string // and a new arg list that can be executed by a database. The `query` should // use the `?` bindVar. The return value uses the `?` bindVar. @@ -113,17 +151,25 @@ func In(query string, args ...interface{}) (string, []interface{}, error) { var flatArgsCount int var anySlices bool - meta := make([]argMeta, len(args)) + var stackMeta [32]argMeta + + var meta []argMeta + if len(args) <= len(stackMeta) { + meta = stackMeta[:len(args)] + } else { + meta = make([]argMeta, len(args)) + } for i, arg := range args { if a, ok := arg.(driver.Valuer); ok { - arg, _ = a.Value() + var err error + arg, err = a.Value() + if err != nil { + return "", nil, err + } } - v := reflect.ValueOf(arg) - t := reflectx.Deref(v.Type()) - // []byte is a driver.Value type so it should not be expanded - if t.Kind() == reflect.Slice && t != reflect.TypeOf([]byte{}) { + if v, ok := asSliceForIn(arg); ok { meta[i].length = v.Len() meta[i].v = v @@ -146,7 +192,9 @@ func In(query string, args ...interface{}) (string, []interface{}, error) { } newArgs := make([]interface{}, 0, flatArgsCount) - buf := make([]byte, 0, len(query)+len(", ?")*flatArgsCount) + + var buf strings.Builder + buf.Grow(len(query) + len(", ?")*flatArgsCount) var arg, offset int @@ -172,10 +220,10 @@ func In(query string, args ...interface{}) (string, []interface{}, error) { } // write everything up to and including our ? character - buf = append(buf, query[:offset+i+1]...) + buf.WriteString(query[:offset+i+1]) for si := 1; si < argMeta.length; si++ { - buf = append(buf, ", ?"...) + buf.WriteString(", ?") } newArgs = appendReflectSlice(newArgs, argMeta.v, argMeta.length) @@ -186,13 +234,13 @@ func In(query string, args ...interface{}) (string, []interface{}, error) { offset = 0 } - buf = append(buf, query...) + buf.WriteString(query) if arg < len(meta) { return "", nil, errors.New("number of bindVars less than number arguments") } - return string(buf), newArgs, nil + return buf.String(), newArgs, nil } func appendReflectSlice(args []interface{}, v reflect.Value, vlen int) []interface{} { diff --git a/vendor/github.com/jmoiron/sqlx/named.go b/vendor/github.com/jmoiron/sqlx/named.go index fa82b560..728aa04d 100644 --- a/vendor/github.com/jmoiron/sqlx/named.go +++ b/vendor/github.com/jmoiron/sqlx/named.go @@ -12,10 +12,12 @@ package sqlx // * bindArgs, bindMapArgs, bindAnyArgs - given a list of names, return an arglist // import ( + "bytes" "database/sql" "errors" "fmt" "reflect" + "regexp" "strconv" "unicode" @@ -144,8 +146,22 @@ func prepareNamed(p namedPreparer, query string) (*NamedStmt, error) { }, nil } +// convertMapStringInterface attempts to convert v to map[string]interface{}. +// Unlike v.(map[string]interface{}), this function works on named types that +// are convertible to map[string]interface{} as well. +func convertMapStringInterface(v interface{}) (map[string]interface{}, bool) { + var m map[string]interface{} + mtype := reflect.TypeOf(m) + t := reflect.TypeOf(v) + if !t.ConvertibleTo(mtype) { + return nil, false + } + return reflect.ValueOf(v).Convert(mtype).Interface().(map[string]interface{}), true + +} + func bindAnyArgs(names []string, arg interface{}, m *reflectx.Mapper) ([]interface{}, error) { - if maparg, ok := arg.(map[string]interface{}); ok { + if maparg, ok := convertMapStringInterface(arg); ok { return bindMapArgs(names, maparg) } return bindArgs(names, arg, m) @@ -200,7 +216,7 @@ func bindStruct(bindType int, query string, arg interface{}, m *reflectx.Mapper) return "", []interface{}{}, err } - arglist, err := bindArgs(names, arg, m) + arglist, err := bindAnyArgs(names, arg, m) if err != nil { return "", []interface{}{}, err } @@ -208,6 +224,82 @@ func bindStruct(bindType int, query string, arg interface{}, m *reflectx.Mapper) return bound, arglist, nil } +var valuesReg = regexp.MustCompile(`\)\s*(?i)VALUES\s*\(`) + +func findMatchingClosingBracketIndex(s string) int { + count := 0 + for i, ch := range s { + if ch == '(' { + count++ + } + if ch == ')' { + count-- + if count == 0 { + return i + } + } + } + return 0 +} + +func fixBound(bound string, loop int) string { + loc := valuesReg.FindStringIndex(bound) + // defensive guard when "VALUES (...)" not found + if len(loc) < 2 { + return bound + } + + openingBracketIndex := loc[1] - 1 + index := findMatchingClosingBracketIndex(bound[openingBracketIndex:]) + // defensive guard. must have closing bracket + if index == 0 { + return bound + } + closingBracketIndex := openingBracketIndex + index + 1 + + var buffer bytes.Buffer + + buffer.WriteString(bound[0:closingBracketIndex]) + for i := 0; i < loop-1; i++ { + buffer.WriteString(",") + buffer.WriteString(bound[openingBracketIndex:closingBracketIndex]) + } + buffer.WriteString(bound[closingBracketIndex:]) + return buffer.String() +} + +// bindArray binds a named parameter query with fields from an array or slice of +// structs argument. +func bindArray(bindType int, query string, arg interface{}, m *reflectx.Mapper) (string, []interface{}, error) { + // do the initial binding with QUESTION; if bindType is not question, + // we can rebind it at the end. + bound, names, err := compileNamedQuery([]byte(query), QUESTION) + if err != nil { + return "", []interface{}{}, err + } + arrayValue := reflect.ValueOf(arg) + arrayLen := arrayValue.Len() + if arrayLen == 0 { + return "", []interface{}{}, fmt.Errorf("length of array is 0: %#v", arg) + } + var arglist = make([]interface{}, 0, len(names)*arrayLen) + for i := 0; i < arrayLen; i++ { + elemArglist, err := bindAnyArgs(names, arrayValue.Index(i).Interface(), m) + if err != nil { + return "", []interface{}{}, err + } + arglist = append(arglist, elemArglist...) + } + if arrayLen > 1 { + bound = fixBound(bound, arrayLen) + } + // adjust binding type if we weren't on question + if bindType != QUESTION { + bound = Rebind(bindType, bound) + } + return bound, arglist, nil +} + // bindMap binds a named parameter query with a map of arguments. func bindMap(bindType int, query string, args map[string]interface{}) (string, []interface{}, error) { bound, names, err := compileNamedQuery([]byte(query), bindType) @@ -259,7 +351,7 @@ func compileNamedQuery(qs []byte, bindType int) (query string, names []string, e } inName = true name = []byte{} - } else if inName && i > 0 && b == '=' { + } else if inName && i > 0 && b == '=' && len(name) == 0 { rebound = append(rebound, ':', '=') inName = false continue @@ -327,10 +419,20 @@ func Named(query string, arg interface{}) (string, []interface{}, error) { } func bindNamedMapper(bindType int, query string, arg interface{}, m *reflectx.Mapper) (string, []interface{}, error) { - if maparg, ok := arg.(map[string]interface{}); ok { - return bindMap(bindType, query, maparg) + t := reflect.TypeOf(arg) + k := t.Kind() + switch { + case k == reflect.Map && t.Key().Kind() == reflect.String: + m, ok := convertMapStringInterface(arg) + if !ok { + return "", nil, fmt.Errorf("sqlx.bindNamedMapper: unsupported map type: %T", arg) + } + return bindMap(bindType, query, m) + case k == reflect.Array || k == reflect.Slice: + return bindArray(bindType, query, arg, m) + default: + return bindStruct(bindType, query, arg, m) } - return bindStruct(bindType, query, arg, m) } // NamedQuery binds a named query and then runs Query on the result using the @@ -346,7 +448,7 @@ func NamedQuery(e Ext, query string, arg interface{}) (*Rows, error) { // NamedExec uses BindStruct to get a query executable by the driver and // then runs Exec on the result. Returns an error from the binding -// or the query excution itself. +// or the query execution itself. func NamedExec(e Ext, query string, arg interface{}) (sql.Result, error) { q, args, err := bindNamedMapper(BindType(e.DriverName()), query, arg, mapperFor(e)) if err != nil { diff --git a/vendor/github.com/jmoiron/sqlx/named_context.go b/vendor/github.com/jmoiron/sqlx/named_context.go index 9405007e..07ad2165 100644 --- a/vendor/github.com/jmoiron/sqlx/named_context.go +++ b/vendor/github.com/jmoiron/sqlx/named_context.go @@ -122,7 +122,7 @@ func NamedQueryContext(ctx context.Context, e ExtContext, query string, arg inte // NamedExecContext uses BindStruct to get a query executable by the driver and // then runs Exec on the result. Returns an error from the binding -// or the query excution itself. +// or the query execution itself. func NamedExecContext(ctx context.Context, e ExtContext, query string, arg interface{}) (sql.Result, error) { q, args, err := bindNamedMapper(BindType(e.DriverName()), query, arg, mapperFor(e)) if err != nil { diff --git a/vendor/github.com/jmoiron/sqlx/reflectx/reflect.go b/vendor/github.com/jmoiron/sqlx/reflectx/reflect.go index 73c21eb3..0b109942 100644 --- a/vendor/github.com/jmoiron/sqlx/reflectx/reflect.go +++ b/vendor/github.com/jmoiron/sqlx/reflectx/reflect.go @@ -269,9 +269,7 @@ type typeQueue struct { // A copying append that creates a new slice each time. func apnd(is []int, i int) []int { x := make([]int, len(is)+1) - for p, n := range is { - x[p] = n - } + copy(x, is) x[len(x)-1] = i return x } @@ -431,9 +429,14 @@ QueueLoop: flds := &StructMap{Index: m, Tree: root, Paths: map[string]*FieldInfo{}, Names: map[string]*FieldInfo{}} for _, fi := range flds.Index { - flds.Paths[fi.Path] = fi - if fi.Name != "" && !fi.Embedded { - flds.Names[fi.Path] = fi + // check if nothing has already been pushed with the same path + // sometimes you can choose to override a type using embedded struct + fld, ok := flds.Paths[fi.Path] + if !ok || fld.Embedded { + flds.Paths[fi.Path] = fi + if fi.Name != "" && !fi.Embedded { + flds.Names[fi.Path] = fi + } } } diff --git a/vendor/github.com/jmoiron/sqlx/sqlx.go b/vendor/github.com/jmoiron/sqlx/sqlx.go index 3f000f47..f7b28768 100644 --- a/vendor/github.com/jmoiron/sqlx/sqlx.go +++ b/vendor/github.com/jmoiron/sqlx/sqlx.go @@ -64,11 +64,7 @@ func isScannable(t reflect.Type) bool { // it's not important that we use the right mapper for this particular object, // we're only concerned on how many exported fields this struct has - m := mapper() - if len(m.TypeMap(t).Index) == 0 { - return true - } - return false + return len(mapper().TypeMap(t).Index) == 0 } // ColScanner is an interface used by MapScan and SliceScan @@ -149,15 +145,15 @@ func isUnsafe(i interface{}) bool { } func mapperFor(i interface{}) *reflectx.Mapper { - switch i.(type) { + switch i := i.(type) { case DB: - return i.(DB).Mapper + return i.Mapper case *DB: - return i.(*DB).Mapper + return i.Mapper case Tx: - return i.(Tx).Mapper + return i.Mapper case *Tx: - return i.(*Tx).Mapper + return i.Mapper default: return mapper() } @@ -380,6 +376,14 @@ func (db *DB) PrepareNamed(query string) (*NamedStmt, error) { return prepareNamed(db, query) } +// Conn is a wrapper around sql.Conn with extra functionality +type Conn struct { + *sql.Conn + driverName string + unsafe bool + Mapper *reflectx.Mapper +} + // Tx is an sqlx wrapper around sql.Tx with extra functionality type Tx struct { *sql.Tx @@ -874,9 +878,10 @@ func structOnlyError(t reflect.Type) error { } // scanAll scans all rows into a destination, which must be a slice of any -// type. If the destination slice type is a Struct, then StructScan will be -// used on each row. If the destination is some other kind of base type, then -// each row must only have one column which can scan into that type. This +// type. It resets the slice length to zero before appending each element to +// the slice. If the destination slice type is a Struct, then StructScan will +// be used on each row. If the destination is some other kind of base type, +// then each row must only have one column which can scan into that type. This // allows you to do something like: // // rows, _ := db.Query("select id from people;") @@ -906,6 +911,7 @@ func scanAll(rows rowsi, dest interface{}, structOnly bool) error { if err != nil { return err } + direct.SetLen(0) isPtr := slice.Elem().Kind() == reflect.Ptr base := reflectx.Deref(slice.Elem()) diff --git a/vendor/github.com/jmoiron/sqlx/sqlx_context.go b/vendor/github.com/jmoiron/sqlx/sqlx_context.go index 06033111..7aa4dd01 100644 --- a/vendor/github.com/jmoiron/sqlx/sqlx_context.go +++ b/vendor/github.com/jmoiron/sqlx/sqlx_context.go @@ -208,6 +208,74 @@ func (db *DB) BeginTxx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) { return &Tx{Tx: tx, driverName: db.driverName, unsafe: db.unsafe, Mapper: db.Mapper}, err } +// Connx returns an *sqlx.Conn instead of an *sql.Conn. +func (db *DB) Connx(ctx context.Context) (*Conn, error) { + conn, err := db.DB.Conn(ctx) + if err != nil { + return nil, err + } + + return &Conn{Conn: conn, driverName: db.driverName, unsafe: db.unsafe, Mapper: db.Mapper}, nil +} + +// BeginTxx begins a transaction and returns an *sqlx.Tx instead of an +// *sql.Tx. +// +// The provided context is used until the transaction is committed or rolled +// back. If the context is canceled, the sql package will roll back the +// transaction. Tx.Commit will return an error if the context provided to +// BeginxContext is canceled. +func (c *Conn) BeginTxx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) { + tx, err := c.Conn.BeginTx(ctx, opts) + if err != nil { + return nil, err + } + return &Tx{Tx: tx, driverName: c.driverName, unsafe: c.unsafe, Mapper: c.Mapper}, err +} + +// SelectContext using this Conn. +// Any placeholder parameters are replaced with supplied args. +func (c *Conn) SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error { + return SelectContext(ctx, c, dest, query, args...) +} + +// GetContext using this Conn. +// Any placeholder parameters are replaced with supplied args. +// An error is returned if the result set is empty. +func (c *Conn) GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error { + return GetContext(ctx, c, dest, query, args...) +} + +// PreparexContext returns an sqlx.Stmt instead of a sql.Stmt. +// +// The provided context is used for the preparation of the statement, not for +// the execution of the statement. +func (c *Conn) PreparexContext(ctx context.Context, query string) (*Stmt, error) { + return PreparexContext(ctx, c, query) +} + +// QueryxContext queries the database and returns an *sqlx.Rows. +// Any placeholder parameters are replaced with supplied args. +func (c *Conn) QueryxContext(ctx context.Context, query string, args ...interface{}) (*Rows, error) { + r, err := c.Conn.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + return &Rows{Rows: r, unsafe: c.unsafe, Mapper: c.Mapper}, err +} + +// QueryRowxContext queries the database and returns an *sqlx.Row. +// Any placeholder parameters are replaced with supplied args. +func (c *Conn) QueryRowxContext(ctx context.Context, query string, args ...interface{}) *Row { + rows, err := c.Conn.QueryContext(ctx, query, args...) + return &Row{rows: rows, err: err, unsafe: c.unsafe, Mapper: c.Mapper} +} + +// Rebind a query within a Conn's bindvar type. +func (c *Conn) Rebind(query string) string { + return Rebind(BindType(c.driverName), query) +} + // StmtxContext returns a version of the prepared statement which runs within a // transaction. Provided stmt can be either *sql.Stmt or *sqlx.Stmt. func (tx *Tx) StmtxContext(ctx context.Context, stmt interface{}) *Stmt { diff --git a/vendor/github.com/lib/pq/.travis.sh b/vendor/github.com/lib/pq/.travis.sh deleted file mode 100644 index 15607b50..00000000 --- a/vendor/github.com/lib/pq/.travis.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -set -eux - -client_configure() { - sudo chmod 600 $PQSSLCERTTEST_PATH/postgresql.key -} - -pgdg_repository() { - curl -sS 'https://www.postgresql.org/media/keys/ACCC4CF8.asc' | sudo apt-key add - - echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list - sudo apt-get update -} - -postgresql_configure() { - sudo tee /etc/postgresql/$PGVERSION/main/pg_hba.conf > /dev/null <<-config - local all all trust - hostnossl all pqgossltest 127.0.0.1/32 reject - hostnossl all pqgosslcert 127.0.0.1/32 reject - hostssl all pqgossltest 127.0.0.1/32 trust - hostssl all pqgosslcert 127.0.0.1/32 cert - host all all 127.0.0.1/32 trust - hostnossl all pqgossltest ::1/128 reject - hostnossl all pqgosslcert ::1/128 reject - hostssl all pqgossltest ::1/128 trust - hostssl all pqgosslcert ::1/128 cert - host all all ::1/128 trust - config - - xargs sudo install -o postgres -g postgres -m 600 -t /var/lib/postgresql/$PGVERSION/main/ <<-certificates - certs/root.crt - certs/server.crt - certs/server.key - certificates - - sort -VCu <<-versions || - $PGVERSION - 9.2 - versions - sudo tee -a /etc/postgresql/$PGVERSION/main/postgresql.conf > /dev/null <<-config - ssl_ca_file = 'root.crt' - ssl_cert_file = 'server.crt' - ssl_key_file = 'server.key' - config - - echo 127.0.0.1 postgres | sudo tee -a /etc/hosts > /dev/null - - sudo service postgresql restart -} - -postgresql_install() { - xargs sudo apt-get -y install <<-packages - postgresql-$PGVERSION - postgresql-client-$PGVERSION - postgresql-server-dev-$PGVERSION - packages -} - -postgresql_uninstall() { - sudo service postgresql stop - xargs sudo apt-get -y --purge remove <<-packages - libpq-dev - libpq5 - postgresql - postgresql-client-common - postgresql-common - packages - sudo rm -rf /var/lib/postgresql -} - -$1 diff --git a/vendor/github.com/lib/pq/.travis.yml b/vendor/github.com/lib/pq/.travis.yml deleted file mode 100644 index 283f35f2..00000000 --- a/vendor/github.com/lib/pq/.travis.yml +++ /dev/null @@ -1,45 +0,0 @@ -language: go - -go: - - 1.14.x - - 1.15.x - - 1.16.x - -sudo: true - -env: - global: - - PGUSER=postgres - - PQGOSSLTESTS=1 - - PQSSLCERTTEST_PATH=$PWD/certs - - PGHOST=127.0.0.1 - - GODEBUG=x509ignoreCN=0 - matrix: - - PGVERSION=10 - - PGVERSION=9.6 - - PGVERSION=9.5 - - PGVERSION=9.4 - -before_install: - - ./.travis.sh postgresql_uninstall - - ./.travis.sh pgdg_repository - - ./.travis.sh postgresql_install - - ./.travis.sh postgresql_configure - - ./.travis.sh client_configure - - go get golang.org/x/tools/cmd/goimports - - go get golang.org/x/lint/golint - - GO111MODULE=on go get honnef.co/go/tools/cmd/staticcheck@2020.1.3 - -before_script: - - createdb pqgotest - - createuser -DRS pqgossltest - - createuser -DRS pqgosslcert - -script: - - > - goimports -d -e $(find -name '*.go') | awk '{ print } END { exit NR == 0 ? 0 : 1 }' - - go vet ./... - - staticcheck -go 1.13 ./... - - golint ./... - - PQTEST_BINARY_PARAMETERS=no go test -race -v ./... - - PQTEST_BINARY_PARAMETERS=yes go test -race -v ./... diff --git a/vendor/github.com/lib/pq/README.md b/vendor/github.com/lib/pq/README.md index c972a86a..126ee5d3 100644 --- a/vendor/github.com/lib/pq/README.md +++ b/vendor/github.com/lib/pq/README.md @@ -27,4 +27,10 @@ ## Status -This package is effectively in maintenance mode and is not actively developed. Small patches and features are only rarely reviewed and merged. We recommend using [pgx](https://github.com/jackc/pgx) which is actively maintained. +This package is currently in maintenance mode, which means: +1. It generally does not accept new features. +2. It does accept bug fixes and version compatability changes provided by the community. +3. Maintainers usually do not resolve reported issues. +4. Community members are encouraged to help each other with reported issues. + +For users that require new features or reliable resolution of reported bugs, we recommend using [pgx](https://github.com/jackc/pgx) which is under active development. diff --git a/vendor/github.com/lib/pq/array.go b/vendor/github.com/lib/pq/array.go index 7806a31f..39c8f7e2 100644 --- a/vendor/github.com/lib/pq/array.go +++ b/vendor/github.com/lib/pq/array.go @@ -587,8 +587,8 @@ func (a *Int32Array) scanBytes(src []byte) error { } else { b := make(Int32Array, len(elems)) for i, v := range elems { - var x int - if x, err = strconv.Atoi(string(v)); err != nil { + x, err := strconv.ParseInt(string(v), 10, 32) + if err != nil { return fmt.Errorf("pq: parsing array element index %d: %v", i, err) } b[i] = int32(x) diff --git a/vendor/github.com/lib/pq/conn.go b/vendor/github.com/lib/pq/conn.go index b09a1704..da4ff9de 100644 --- a/vendor/github.com/lib/pq/conn.go +++ b/vendor/github.com/lib/pq/conn.go @@ -2,6 +2,7 @@ package pq import ( "bufio" + "bytes" "context" "crypto/md5" "crypto/sha256" @@ -18,7 +19,7 @@ import ( "path/filepath" "strconv" "strings" - "sync/atomic" + "sync" "time" "unicode" @@ -31,8 +32,10 @@ var ( ErrNotSupported = errors.New("pq: Unsupported command") ErrInFailedTransaction = errors.New("pq: Could not complete operation in a failed transaction") ErrSSLNotSupported = errors.New("pq: SSL is not enabled on the server") - ErrSSLKeyHasWorldPermissions = errors.New("pq: Private key file has group or world access. Permissions should be u=rw (0600) or less") - ErrCouldNotDetectUsername = errors.New("pq: Could not detect default username. Please provide one explicitly") + ErrSSLKeyUnknownOwnership = errors.New("pq: Could not get owner information for private key, may not be properly protected") + ErrSSLKeyHasWorldPermissions = errors.New("pq: Private key has world access. Permissions should be u=rw,g=r (0640) if owned by root, or u=rw (0600), or less") + + ErrCouldNotDetectUsername = errors.New("pq: Could not detect default username. Please provide one explicitly") errUnexpectedReady = errors.New("unexpected ReadyForQuery") errNoRowsAffected = errors.New("no RowsAffected available after the empty statement") @@ -110,7 +113,9 @@ type defaultDialer struct { func (d defaultDialer) Dial(network, address string) (net.Conn, error) { return d.d.Dial(network, address) } -func (d defaultDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { +func (d defaultDialer) DialTimeout( + network, address string, timeout time.Duration, +) (net.Conn, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() return d.DialContext(ctx, network, address) @@ -140,9 +145,10 @@ type conn struct { saveMessageType byte saveMessageBuffer []byte - // If true, this connection is bad and all public-facing functions should - // return ErrBadConn. - bad *atomic.Value + // If an error is set, this connection is bad and all public-facing + // functions should return the appropriate error by calling get() + // (ErrBadConn) or getForNext(). + err syncErr // If set, this connection should never use the binary format when // receiving query results from prepared statements. Only provided for @@ -166,6 +172,40 @@ type conn struct { gss GSS } +type syncErr struct { + err error + sync.Mutex +} + +// Return ErrBadConn if connection is bad. +func (e *syncErr) get() error { + e.Lock() + defer e.Unlock() + if e.err != nil { + return driver.ErrBadConn + } + return nil +} + +// Return the error set on the connection. Currently only used by rows.Next. +func (e *syncErr) getForNext() error { + e.Lock() + defer e.Unlock() + return e.err +} + +// Set error, only if it isn't set yet. +func (e *syncErr) set(err error) { + if err == nil { + panic("attempt to set nil err") + } + e.Lock() + defer e.Unlock() + if e.err == nil { + e.err = err + } +} + // Handle driver-side settings in parsed connection string. func (cn *conn) handleDriverSettings(o values) (err error) { boolSetting := func(key string, val *bool) error { @@ -223,47 +263,56 @@ func (cn *conn) handlePgpass(o values) { } defer file.Close() scanner := bufio.NewScanner(io.Reader(file)) + // From: https://github.com/tg/pgpass/blob/master/reader.go + for scanner.Scan() { + if scanText(scanner.Text(), o) { + break + } + } +} + +// GetFields is a helper function for scanText. +func getFields(s string) []string { + fs := make([]string, 0, 5) + f := make([]rune, 0, len(s)) + + var esc bool + for _, c := range s { + switch { + case esc: + f = append(f, c) + esc = false + case c == '\\': + esc = true + case c == ':': + fs = append(fs, string(f)) + f = f[:0] + default: + f = append(f, c) + } + } + return append(fs, string(f)) +} + +// ScanText assists HandlePgpass in it's objective. +func scanText(line string, o values) bool { hostname := o["host"] ntw, _ := network(o) port := o["port"] db := o["dbname"] username := o["user"] - // From: https://github.com/tg/pgpass/blob/master/reader.go - getFields := func(s string) []string { - fs := make([]string, 0, 5) - f := make([]rune, 0, len(s)) - - var esc bool - for _, c := range s { - switch { - case esc: - f = append(f, c) - esc = false - case c == '\\': - esc = true - case c == ':': - fs = append(fs, string(f)) - f = f[:0] - default: - f = append(f, c) - } - } - return append(fs, string(f)) + if len(line) == 0 || line[0] == '#' { + return false } - for scanner.Scan() { - line := scanner.Text() - if len(line) == 0 || line[0] == '#' { - continue - } - split := getFields(line) - if len(split) != 5 { - continue - } - if (split[0] == "*" || split[0] == hostname || (split[0] == "localhost" && (hostname == "" || ntw == "unix"))) && (split[1] == "*" || split[1] == port) && (split[2] == "*" || split[2] == db) && (split[3] == "*" || split[3] == username) { - o["password"] = split[4] - return - } + split := getFields(line) + if len(split) != 5 { + return false } + if (split[0] == "*" || split[0] == hostname || (split[0] == "localhost" && (hostname == "" || ntw == "unix"))) && (split[1] == "*" || split[1] == port) && (split[2] == "*" || split[2] == db) && (split[3] == "*" || split[3] == username) { + o["password"] = split[4] + return true + } + return false } func (cn *conn) writeBuf(b byte) *writeBuf { @@ -287,7 +336,7 @@ func DialOpen(d Dialer, dsn string) (_ driver.Conn, err error) { if err != nil { return nil, err } - c.dialer = d + c.Dialer(d) return c.open(context.Background()) } @@ -306,12 +355,9 @@ func (c *Connector) open(ctx context.Context) (cn *conn, err error) { o[k] = v } - bad := &atomic.Value{} - bad.Store(false) cn = &conn{ opts: o, dialer: c.dialer, - bad: bad, } err = cn.handleDriverSettings(o) if err != nil { @@ -516,22 +562,9 @@ func (cn *conn) isInTransaction() bool { cn.txnStatus == txnStatusInFailedTransaction } -func (cn *conn) setBad() { - if cn.bad != nil { - cn.bad.Store(true) - } -} - -func (cn *conn) getBad() bool { - if cn.bad != nil { - return cn.bad.Load().(bool) - } - return false -} - func (cn *conn) checkIsInTransaction(intxn bool) { if cn.isInTransaction() != intxn { - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected transaction status %v", cn.txnStatus) } } @@ -541,8 +574,8 @@ func (cn *conn) Begin() (_ driver.Tx, err error) { } func (cn *conn) begin(mode string) (_ driver.Tx, err error) { - if cn.getBad() { - return nil, driver.ErrBadConn + if err := cn.err.get(); err != nil { + return nil, err } defer cn.errRecover(&err) @@ -552,11 +585,11 @@ func (cn *conn) begin(mode string) (_ driver.Tx, err error) { return nil, err } if commandTag != "BEGIN" { - cn.setBad() + cn.err.set(driver.ErrBadConn) return nil, fmt.Errorf("unexpected command tag %s", commandTag) } if cn.txnStatus != txnStatusIdleInTransaction { - cn.setBad() + cn.err.set(driver.ErrBadConn) return nil, fmt.Errorf("unexpected transaction status %v", cn.txnStatus) } return cn, nil @@ -570,8 +603,8 @@ func (cn *conn) closeTxn() { func (cn *conn) Commit() (err error) { defer cn.closeTxn() - if cn.getBad() { - return driver.ErrBadConn + if err := cn.err.get(); err != nil { + return err } defer cn.errRecover(&err) @@ -592,12 +625,12 @@ func (cn *conn) Commit() (err error) { _, commandTag, err := cn.simpleExec("COMMIT") if err != nil { if cn.isInTransaction() { - cn.setBad() + cn.err.set(driver.ErrBadConn) } return err } if commandTag != "COMMIT" { - cn.setBad() + cn.err.set(driver.ErrBadConn) return fmt.Errorf("unexpected command tag %s", commandTag) } cn.checkIsInTransaction(false) @@ -606,8 +639,8 @@ func (cn *conn) Commit() (err error) { func (cn *conn) Rollback() (err error) { defer cn.closeTxn() - if cn.getBad() { - return driver.ErrBadConn + if err := cn.err.get(); err != nil { + return err } defer cn.errRecover(&err) return cn.rollback() @@ -618,7 +651,7 @@ func (cn *conn) rollback() (err error) { _, commandTag, err := cn.simpleExec("ROLLBACK") if err != nil { if cn.isInTransaction() { - cn.setBad() + cn.err.set(driver.ErrBadConn) } return err } @@ -658,7 +691,7 @@ func (cn *conn) simpleExec(q string) (res driver.Result, commandTag string, err case 'T', 'D': // ignore any results default: - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unknown response for simple query: %q", t) } } @@ -680,7 +713,7 @@ func (cn *conn) simpleQuery(q string) (res *rows, err error) { // the user can close, though, to avoid connections from being // leaked. A "rows" with done=true works fine for that purpose. if err != nil { - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected message %q in simple query execution", t) } if res == nil { @@ -707,7 +740,7 @@ func (cn *conn) simpleQuery(q string) (res *rows, err error) { err = parseError(r) case 'D': if res == nil { - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected DataRow in simple query execution") } // the query didn't fail; kick off to Next @@ -722,7 +755,7 @@ func (cn *conn) simpleQuery(q string) (res *rows, err error) { // To work around a bug in QueryRow in Go 1.2 and earlier, wait // until the first DataRow has been received. default: - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unknown response for simple query: %q", t) } } @@ -744,7 +777,9 @@ func (noRows) RowsAffected() (int64, error) { // Decides which column formats to use for a prepared statement. The input is // an array of type oids, one element per result column. -func decideColumnFormats(colTyps []fieldDesc, forceText bool) (colFmts []format, colFmtData []byte) { +func decideColumnFormats( + colTyps []fieldDesc, forceText bool, +) (colFmts []format, colFmtData []byte) { if len(colTyps) == 0 { return nil, colFmtDataAllText } @@ -815,8 +850,8 @@ func (cn *conn) prepareTo(q, stmtName string) *stmt { } func (cn *conn) Prepare(q string) (_ driver.Stmt, err error) { - if cn.getBad() { - return nil, driver.ErrBadConn + if err := cn.err.get(); err != nil { + return nil, err } defer cn.errRecover(&err) @@ -854,8 +889,8 @@ func (cn *conn) Query(query string, args []driver.Value) (driver.Rows, error) { } func (cn *conn) query(query string, args []driver.Value) (_ *rows, err error) { - if cn.getBad() { - return nil, driver.ErrBadConn + if err := cn.err.get(); err != nil { + return nil, err } if cn.inCopy { return nil, errCopyInProgress @@ -888,8 +923,8 @@ func (cn *conn) query(query string, args []driver.Value) (_ *rows, err error) { // Implement the optional "Execer" interface for one-shot queries func (cn *conn) Exec(query string, args []driver.Value) (res driver.Result, err error) { - if cn.getBad() { - return nil, driver.ErrBadConn + if err := cn.err.get(); err != nil { + return nil, err } defer cn.errRecover(&err) @@ -960,7 +995,7 @@ func (cn *conn) sendSimpleMessage(typ byte) (err error) { // the message yourself. func (cn *conn) saveMessage(typ byte, buf *readBuf) { if cn.saveMessageType != 0 { - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected saveMessageType %d", cn.saveMessageType) } cn.saveMessageType = typ @@ -1106,7 +1141,7 @@ func isDriverSetting(key string) bool { return true case "password": return true - case "sslmode", "sslcert", "sslkey", "sslrootcert", "sslinline": + case "sslmode", "sslcert", "sslkey", "sslrootcert", "sslinline", "sslsni": return true case "fallback_application_name": return true @@ -1330,8 +1365,8 @@ func (st *stmt) Close() (err error) { if st.closed { return nil } - if st.cn.getBad() { - return driver.ErrBadConn + if err := st.cn.err.get(); err != nil { + return err } defer st.cn.errRecover(&err) @@ -1344,14 +1379,14 @@ func (st *stmt) Close() (err error) { t, _ := st.cn.recv1() if t != '3' { - st.cn.setBad() + st.cn.err.set(driver.ErrBadConn) errorf("unexpected close response: %q", t) } st.closed = true t, r := st.cn.recv1() if t != 'Z' { - st.cn.setBad() + st.cn.err.set(driver.ErrBadConn) errorf("expected ready for query, but got: %q", t) } st.cn.processReadyForQuery(r) @@ -1360,8 +1395,12 @@ func (st *stmt) Close() (err error) { } func (st *stmt) Query(v []driver.Value) (r driver.Rows, err error) { - if st.cn.getBad() { - return nil, driver.ErrBadConn + return st.query(v) +} + +func (st *stmt) query(v []driver.Value) (r *rows, err error) { + if err := st.cn.err.get(); err != nil { + return nil, err } defer st.cn.errRecover(&err) @@ -1373,8 +1412,8 @@ func (st *stmt) Query(v []driver.Value) (r driver.Rows, err error) { } func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) { - if st.cn.getBad() { - return nil, driver.ErrBadConn + if err := st.cn.err.get(); err != nil { + return nil, err } defer st.cn.errRecover(&err) @@ -1460,7 +1499,7 @@ func (cn *conn) parseComplete(commandTag string) (driver.Result, string) { if affectedRows == nil && strings.HasPrefix(commandTag, "INSERT ") { parts := strings.Split(commandTag, " ") if len(parts) != 3 { - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected INSERT command tag %s", commandTag) } affectedRows = &parts[len(parts)-1] @@ -1472,7 +1511,7 @@ func (cn *conn) parseComplete(commandTag string) (driver.Result, string) { } n, err := strconv.ParseInt(*affectedRows, 10, 64) if err != nil { - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("could not parse commandTag: %s", err) } return driver.RowsAffected(n), commandTag @@ -1539,8 +1578,8 @@ func (rs *rows) Next(dest []driver.Value) (err error) { } conn := rs.cn - if conn.getBad() { - return driver.ErrBadConn + if err := conn.err.getForNext(); err != nil { + return err } defer conn.errRecover(&err) @@ -1564,7 +1603,7 @@ func (rs *rows) Next(dest []driver.Value) (err error) { case 'D': n := rs.rb.int16() if err != nil { - conn.setBad() + conn.err.set(driver.ErrBadConn) errorf("unexpected DataRow after error %s", err) } if n < len(dest) { @@ -1606,10 +1645,10 @@ func (rs *rows) NextResultSet() error { // QuoteIdentifier quotes an "identifier" (e.g. a table or a column name) to be // used as part of an SQL statement. For example: // -// tblname := "my_table" -// data := "my_data" -// quoted := pq.QuoteIdentifier(tblname) -// err := db.Exec(fmt.Sprintf("INSERT INTO %s VALUES ($1)", quoted), data) +// tblname := "my_table" +// data := "my_data" +// quoted := pq.QuoteIdentifier(tblname) +// err := db.Exec(fmt.Sprintf("INSERT INTO %s VALUES ($1)", quoted), data) // // Any double quotes in name will be escaped. The quoted identifier will be // case sensitive when used in a query. If the input string contains a zero @@ -1622,12 +1661,24 @@ func QuoteIdentifier(name string) string { return `"` + strings.Replace(name, `"`, `""`, -1) + `"` } +// BufferQuoteIdentifier satisfies the same purpose as QuoteIdentifier, but backed by a +// byte buffer. +func BufferQuoteIdentifier(name string, buffer *bytes.Buffer) { + end := strings.IndexRune(name, 0) + if end > -1 { + name = name[:end] + } + buffer.WriteRune('"') + buffer.WriteString(strings.Replace(name, `"`, `""`, -1)) + buffer.WriteRune('"') +} + // QuoteLiteral quotes a 'literal' (e.g. a parameter, often used to pass literal // to DDL and other statements that do not accept parameters) to be used as part // of an SQL statement. For example: // -// exp_date := pq.QuoteLiteral("2023-01-05 15:00:00Z") -// err := db.Exec(fmt.Sprintf("CREATE ROLE my_user VALID UNTIL %s", exp_date)) +// exp_date := pq.QuoteLiteral("2023-01-05 15:00:00Z") +// err := db.Exec(fmt.Sprintf("CREATE ROLE my_user VALID UNTIL %s", exp_date)) // // Any single quotes in name will be escaped. Any backslashes (i.e. "\") will be // replaced by two backslashes (i.e. "\\") and the C-style escape identifier @@ -1758,7 +1809,7 @@ func (cn *conn) readReadyForQuery() { cn.processReadyForQuery(r) return default: - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected message %q; expected ReadyForQuery", t) } } @@ -1778,12 +1829,16 @@ func (cn *conn) readParseResponse() { cn.readReadyForQuery() panic(err) default: - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected Parse response %q", t) } } -func (cn *conn) readStatementDescribeResponse() (paramTyps []oid.Oid, colNames []string, colTyps []fieldDesc) { +func (cn *conn) readStatementDescribeResponse() ( + paramTyps []oid.Oid, + colNames []string, + colTyps []fieldDesc, +) { for { t, r := cn.recv1() switch t { @@ -1803,7 +1858,7 @@ func (cn *conn) readStatementDescribeResponse() (paramTyps []oid.Oid, colNames [ cn.readReadyForQuery() panic(err) default: - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected Describe statement response %q", t) } } @@ -1821,7 +1876,7 @@ func (cn *conn) readPortalDescribeResponse() rowsHeader { cn.readReadyForQuery() panic(err) default: - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected Describe response %q", t) } panic("not reached") @@ -1837,7 +1892,7 @@ func (cn *conn) readBindResponse() { cn.readReadyForQuery() panic(err) default: - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected Bind response %q", t) } } @@ -1864,20 +1919,22 @@ func (cn *conn) postExecuteWorkaround() { cn.saveMessage(t, r) return default: - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected message during extended query execution: %q", t) } } } // Only for Exec(), since we ignore the returned data -func (cn *conn) readExecuteResponse(protocolState string) (res driver.Result, commandTag string, err error) { +func (cn *conn) readExecuteResponse( + protocolState string, +) (res driver.Result, commandTag string, err error) { for { t, r := cn.recv1() switch t { case 'C': if err != nil { - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected CommandComplete after error %s", err) } res, commandTag = cn.parseComplete(r.string()) @@ -1891,7 +1948,7 @@ func (cn *conn) readExecuteResponse(protocolState string) (res driver.Result, co err = parseError(r) case 'T', 'D', 'I': if err != nil { - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unexpected %q after error %s", t, err) } if t == 'I' { @@ -1899,7 +1956,7 @@ func (cn *conn) readExecuteResponse(protocolState string) (res driver.Result, co } // ignore any results default: - cn.setBad() + cn.err.set(driver.ErrBadConn) errorf("unknown %s response: %q", protocolState, t) } } @@ -1995,6 +2052,8 @@ func parseEnviron(env []string) (out map[string]string) { accrue("sslkey") case "PGSSLROOTCERT": accrue("sslrootcert") + case "PGSSLSNI": + accrue("sslsni") case "PGREQUIRESSL", "PGSSLCRL": unsupported() case "PGREQUIREPEER": @@ -2035,3 +2094,19 @@ func alnumLowerASCII(ch rune) rune { } return -1 // discard } + +// The database/sql/driver package says: +// All Conn implementations should implement the following interfaces: Pinger, SessionResetter, and Validator. +var _ driver.Pinger = &conn{} +var _ driver.SessionResetter = &conn{} + +func (cn *conn) ResetSession(ctx context.Context) error { + // Ensure bad connections are reported: From database/sql/driver: + // If a connection is never returned to the connection pool but immediately reused, then + // ResetSession is called prior to reuse but IsValid is not called. + return cn.err.get() +} + +func (cn *conn) IsValid() bool { + return cn.err.get() == nil +} diff --git a/vendor/github.com/lib/pq/conn_go115.go b/vendor/github.com/lib/pq/conn_go115.go new file mode 100644 index 00000000..f4ef030f --- /dev/null +++ b/vendor/github.com/lib/pq/conn_go115.go @@ -0,0 +1,8 @@ +//go:build go1.15 +// +build go1.15 + +package pq + +import "database/sql/driver" + +var _ driver.Validator = &conn{} diff --git a/vendor/github.com/lib/pq/conn_go18.go b/vendor/github.com/lib/pq/conn_go18.go index 2b9a9599..63d4ca6a 100644 --- a/vendor/github.com/lib/pq/conn_go18.go +++ b/vendor/github.com/lib/pq/conn_go18.go @@ -7,10 +7,13 @@ import ( "fmt" "io" "io/ioutil" - "sync/atomic" "time" ) +const ( + watchCancelDialContextTimeout = time.Second * 10 +) + // Implement the "QueryerContext" interface func (cn *conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { list := make([]driver.Value, len(args)) @@ -43,6 +46,14 @@ func (cn *conn) ExecContext(ctx context.Context, query string, args []driver.Nam return cn.Exec(query, list) } +// Implement the "ConnPrepareContext" interface +func (cn *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { + if finish := cn.watchCancel(ctx); finish != nil { + defer finish() + } + return cn.Prepare(query) +} + // Implement the "ConnBeginTx" interface func (cn *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { var mode string @@ -103,13 +114,13 @@ func (cn *conn) watchCancel(ctx context.Context) func() { } // Set the connection state to bad so it does not get reused. - cn.setBad() + cn.err.set(ctx.Err()) // At this point the function level context is canceled, // so it must not be used for the additional network // request to cancel the query. // Create a new context to pass into the dial. - ctxCancel, cancel := context.WithTimeout(context.Background(), time.Second*10) + ctxCancel, cancel := context.WithTimeout(context.Background(), watchCancelDialContextTimeout) defer cancel() _ = cn.cancel(ctxCancel) @@ -119,7 +130,7 @@ func (cn *conn) watchCancel(ctx context.Context) func() { return func() { select { case <-finished: - cn.setBad() + cn.err.set(ctx.Err()) cn.Close() case finished <- struct{}{}: } @@ -145,11 +156,8 @@ func (cn *conn) cancel(ctx context.Context) error { defer c.Close() { - bad := &atomic.Value{} - bad.Store(false) can := conn{ - c: c, - bad: bad, + c: c, } err = can.ssl(o) if err != nil { @@ -172,3 +180,68 @@ func (cn *conn) cancel(ctx context.Context) error { return err } } + +// Implement the "StmtQueryContext" interface +func (st *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { + list := make([]driver.Value, len(args)) + for i, nv := range args { + list[i] = nv.Value + } + finish := st.watchCancel(ctx) + r, err := st.query(list) + if err != nil { + if finish != nil { + finish() + } + return nil, err + } + r.finish = finish + return r, nil +} + +// Implement the "StmtExecContext" interface +func (st *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { + list := make([]driver.Value, len(args)) + for i, nv := range args { + list[i] = nv.Value + } + + if finish := st.watchCancel(ctx); finish != nil { + defer finish() + } + + return st.Exec(list) +} + +// watchCancel is implemented on stmt in order to not mark the parent conn as bad +func (st *stmt) watchCancel(ctx context.Context) func() { + if done := ctx.Done(); done != nil { + finished := make(chan struct{}) + go func() { + select { + case <-done: + // At this point the function level context is canceled, + // so it must not be used for the additional network + // request to cancel the query. + // Create a new context to pass into the dial. + ctxCancel, cancel := context.WithTimeout(context.Background(), watchCancelDialContextTimeout) + defer cancel() + + _ = st.cancel(ctxCancel) + finished <- struct{}{} + case <-finished: + } + }() + return func() { + select { + case <-finished: + case finished <- struct{}{}: + } + } + } + return nil +} + +func (st *stmt) cancel(ctx context.Context) error { + return st.cn.cancel(ctx) +} diff --git a/vendor/github.com/lib/pq/connector.go b/vendor/github.com/lib/pq/connector.go index d7d47261..1145e122 100644 --- a/vendor/github.com/lib/pq/connector.go +++ b/vendor/github.com/lib/pq/connector.go @@ -27,6 +27,11 @@ func (c *Connector) Connect(ctx context.Context) (driver.Conn, error) { return c.open(ctx) } +// Dialer allows change the dialer used to open connections. +func (c *Connector) Dialer(dialer Dialer) { + c.dialer = dialer +} + // Driver returns the underlying driver of this Connector. func (c *Connector) Driver() driver.Driver { return &Driver{} diff --git a/vendor/github.com/lib/pq/copy.go b/vendor/github.com/lib/pq/copy.go index bb3cbd7b..a8f16b2b 100644 --- a/vendor/github.com/lib/pq/copy.go +++ b/vendor/github.com/lib/pq/copy.go @@ -1,6 +1,8 @@ package pq import ( + "bytes" + "context" "database/sql/driver" "encoding/binary" "errors" @@ -19,29 +21,35 @@ var ( // CopyIn creates a COPY FROM statement which can be prepared with // Tx.Prepare(). The target table should be visible in search_path. func CopyIn(table string, columns ...string) string { - stmt := "COPY " + QuoteIdentifier(table) + " (" + buffer := bytes.NewBufferString("COPY ") + BufferQuoteIdentifier(table, buffer) + buffer.WriteString(" (") + makeStmt(buffer, columns...) + return buffer.String() +} + +// MakeStmt makes the stmt string for CopyIn and CopyInSchema. +func makeStmt(buffer *bytes.Buffer, columns ...string) { + //s := bytes.NewBufferString() for i, col := range columns { if i != 0 { - stmt += ", " + buffer.WriteString(", ") } - stmt += QuoteIdentifier(col) + BufferQuoteIdentifier(col, buffer) } - stmt += ") FROM STDIN" - return stmt + buffer.WriteString(") FROM STDIN") } // CopyInSchema creates a COPY FROM statement which can be prepared with // Tx.Prepare(). func CopyInSchema(schema, table string, columns ...string) string { - stmt := "COPY " + QuoteIdentifier(schema) + "." + QuoteIdentifier(table) + " (" - for i, col := range columns { - if i != 0 { - stmt += ", " - } - stmt += QuoteIdentifier(col) - } - stmt += ") FROM STDIN" - return stmt + buffer := bytes.NewBufferString("COPY ") + BufferQuoteIdentifier(schema, buffer) + buffer.WriteRune('.') + BufferQuoteIdentifier(table, buffer) + buffer.WriteString(" (") + makeStmt(buffer, columns...) + return buffer.String() } type copyin struct { @@ -49,12 +57,14 @@ type copyin struct { buffer []byte rowData chan []byte done chan bool - driver.Result closed bool - sync.Mutex // guards err - err error + mu struct { + sync.Mutex + err error + driver.Result + } } const ciBufferSize = 64 * 1024 @@ -98,13 +108,13 @@ awaitCopyInResponse: err = parseError(r) case 'Z': if err == nil { - ci.setBad() + ci.setBad(driver.ErrBadConn) errorf("unexpected ReadyForQuery in response to COPY") } cn.processReadyForQuery(r) return nil, err default: - ci.setBad() + ci.setBad(driver.ErrBadConn) errorf("unknown response for copy query: %q", t) } } @@ -123,7 +133,7 @@ awaitCopyInResponse: cn.processReadyForQuery(r) return nil, err default: - ci.setBad() + ci.setBad(driver.ErrBadConn) errorf("unknown response for CopyFail: %q", t) } } @@ -144,7 +154,7 @@ func (ci *copyin) resploop() { var r readBuf t, err := ci.cn.recvMessage(&r) if err != nil { - ci.setBad() + ci.setBad(driver.ErrBadConn) ci.setError(err) ci.done <- true return @@ -166,7 +176,7 @@ func (ci *copyin) resploop() { err := parseError(&r) ci.setError(err) default: - ci.setBad() + ci.setBad(driver.ErrBadConn) ci.setError(fmt.Errorf("unknown response during CopyIn: %q", t)) ci.done <- true return @@ -174,46 +184,41 @@ func (ci *copyin) resploop() { } } -func (ci *copyin) setBad() { - ci.Lock() - ci.cn.setBad() - ci.Unlock() +func (ci *copyin) setBad(err error) { + ci.cn.err.set(err) } -func (ci *copyin) isBad() bool { - ci.Lock() - b := ci.cn.getBad() - ci.Unlock() - return b +func (ci *copyin) getBad() error { + return ci.cn.err.get() } -func (ci *copyin) isErrorSet() bool { - ci.Lock() - isSet := (ci.err != nil) - ci.Unlock() - return isSet +func (ci *copyin) err() error { + ci.mu.Lock() + err := ci.mu.err + ci.mu.Unlock() + return err } // setError() sets ci.err if one has not been set already. Caller must not be // holding ci.Mutex. func (ci *copyin) setError(err error) { - ci.Lock() - if ci.err == nil { - ci.err = err + ci.mu.Lock() + if ci.mu.err == nil { + ci.mu.err = err } - ci.Unlock() + ci.mu.Unlock() } func (ci *copyin) setResult(result driver.Result) { - ci.Lock() - ci.Result = result - ci.Unlock() + ci.mu.Lock() + ci.mu.Result = result + ci.mu.Unlock() } func (ci *copyin) getResult() driver.Result { - ci.Lock() - result := ci.Result - ci.Unlock() + ci.mu.Lock() + result := ci.mu.Result + ci.mu.Unlock() if result == nil { return driver.RowsAffected(0) } @@ -240,13 +245,13 @@ func (ci *copyin) Exec(v []driver.Value) (r driver.Result, err error) { return nil, errCopyInClosed } - if ci.isBad() { - return nil, driver.ErrBadConn + if err := ci.getBad(); err != nil { + return nil, err } defer ci.cn.errRecover(&err) - if ci.isErrorSet() { - return nil, ci.err + if err := ci.err(); err != nil { + return nil, err } if len(v) == 0 { @@ -276,14 +281,51 @@ func (ci *copyin) Exec(v []driver.Value) (r driver.Result, err error) { return driver.RowsAffected(0), nil } +// CopyData inserts a raw string into the COPY stream. The insert is +// asynchronous and CopyData can return errors from previous CopyData calls to +// the same COPY stmt. +// +// You need to call Exec(nil) to sync the COPY stream and to get any +// errors from pending data, since Stmt.Close() doesn't return errors +// to the user. +func (ci *copyin) CopyData(ctx context.Context, line string) (r driver.Result, err error) { + if ci.closed { + return nil, errCopyInClosed + } + + if finish := ci.cn.watchCancel(ctx); finish != nil { + defer finish() + } + + if err := ci.getBad(); err != nil { + return nil, err + } + defer ci.cn.errRecover(&err) + + if err := ci.err(); err != nil { + return nil, err + } + + ci.buffer = append(ci.buffer, []byte(line)...) + ci.buffer = append(ci.buffer, '\n') + + if len(ci.buffer) > ciBufferFlushSize { + ci.flush(ci.buffer) + // reset buffer, keep bytes for message identifier and length + ci.buffer = ci.buffer[:5] + } + + return driver.RowsAffected(0), nil +} + func (ci *copyin) Close() (err error) { if ci.closed { // Don't do anything, we're already closed return nil } ci.closed = true - if ci.isBad() { - return driver.ErrBadConn + if err := ci.getBad(); err != nil { + return err } defer ci.cn.errRecover(&err) @@ -299,8 +341,7 @@ func (ci *copyin) Close() (err error) { <-ci.done ci.cn.inCopy = false - if ci.isErrorSet() { - err = ci.err + if err := ci.err(); err != nil { return err } return nil diff --git a/vendor/github.com/lib/pq/encode.go b/vendor/github.com/lib/pq/encode.go index 51c143ee..bffe6096 100644 --- a/vendor/github.com/lib/pq/encode.go +++ b/vendor/github.com/lib/pq/encode.go @@ -422,7 +422,7 @@ func ParseTimestamp(currentLocation *time.Location, str string) (time.Time, erro if remainderIdx < len(str) && str[remainderIdx] == '.' { fracStart := remainderIdx + 1 - fracOff := strings.IndexAny(str[fracStart:], "-+ ") + fracOff := strings.IndexAny(str[fracStart:], "-+Z ") if fracOff < 0 { fracOff = len(str) - fracStart } @@ -432,7 +432,7 @@ func ParseTimestamp(currentLocation *time.Location, str string) (time.Time, erro remainderIdx += fracOff + 1 } if tzStart := remainderIdx; tzStart < len(str) && (str[tzStart] == '-' || str[tzStart] == '+') { - // time zone separator is always '-' or '+' (UTC is +00) + // time zone separator is always '-' or '+' or 'Z' (UTC is +00) var tzSign int switch c := str[tzStart]; c { case '-': @@ -454,7 +454,11 @@ func ParseTimestamp(currentLocation *time.Location, str string) (time.Time, erro remainderIdx += 3 } tzOff = tzSign * ((tzHours * 60 * 60) + (tzMin * 60) + tzSec) + } else if tzStart < len(str) && str[tzStart] == 'Z' { + // time zone Z separator indicates UTC is +00 + remainderIdx += 1 } + var isoYear int if isBC { @@ -559,7 +563,7 @@ func parseBytea(s []byte) (result []byte, err error) { if len(s) < 4 { return nil, fmt.Errorf("invalid bytea sequence %v", s) } - r, err := strconv.ParseInt(string(s[1:4]), 8, 9) + r, err := strconv.ParseUint(string(s[1:4]), 8, 8) if err != nil { return nil, fmt.Errorf("could not parse bytea value: %s", err.Error()) } diff --git a/vendor/github.com/lib/pq/error.go b/vendor/github.com/lib/pq/error.go index c19c349f..f67c5a5f 100644 --- a/vendor/github.com/lib/pq/error.go +++ b/vendor/github.com/lib/pq/error.go @@ -402,6 +402,11 @@ func (err *Error) Fatal() bool { return err.Severity == Efatal } +// SQLState returns the SQLState of the error. +func (err *Error) SQLState() string { + return string(err.Code) +} + // Get implements the legacy PGError interface. New code should use the fields // of the Error struct directly. func (err *Error) Get(k byte) (v string) { @@ -444,7 +449,7 @@ func (err *Error) Get(k byte) (v string) { return "" } -func (err Error) Error() string { +func (err *Error) Error() string { return "pq: " + err.Message } @@ -484,7 +489,7 @@ func (cn *conn) errRecover(err *error) { case nil: // Do nothing case runtime.Error: - cn.setBad() + cn.err.set(driver.ErrBadConn) panic(v) case *Error: if v.Fatal() { @@ -493,26 +498,26 @@ func (cn *conn) errRecover(err *error) { *err = v } case *net.OpError: - cn.setBad() + cn.err.set(driver.ErrBadConn) *err = v case *safeRetryError: - cn.setBad() + cn.err.set(driver.ErrBadConn) *err = driver.ErrBadConn case error: - if v == io.EOF || v.(error).Error() == "remote error: handshake failure" { + if v == io.EOF || v.Error() == "remote error: handshake failure" { *err = driver.ErrBadConn } else { *err = v } default: - cn.setBad() + cn.err.set(driver.ErrBadConn) panic(fmt.Sprintf("unknown error: %#v", e)) } // Any time we return ErrBadConn, we need to remember it since *Tx doesn't // mark the connection bad in database/sql. if *err == driver.ErrBadConn { - cn.setBad() + cn.err.set(driver.ErrBadConn) } } diff --git a/vendor/github.com/lib/pq/notice.go b/vendor/github.com/lib/pq/notice.go index 01dd8c72..70ad122a 100644 --- a/vendor/github.com/lib/pq/notice.go +++ b/vendor/github.com/lib/pq/notice.go @@ -1,3 +1,4 @@ +//go:build go1.10 // +build go1.10 package pq diff --git a/vendor/github.com/lib/pq/ssl.go b/vendor/github.com/lib/pq/ssl.go index e5eb9289..36b61ba4 100644 --- a/vendor/github.com/lib/pq/ssl.go +++ b/vendor/github.com/lib/pq/ssl.go @@ -8,6 +8,7 @@ import ( "os" "os/user" "path/filepath" + "strings" ) // ssl generates a function to upgrade a net.Conn based on the "sslmode" and @@ -50,6 +51,16 @@ func ssl(o values) (func(net.Conn) (net.Conn, error), error) { return nil, fmterrorf(`unsupported sslmode %q; only "require" (default), "verify-full", "verify-ca", and "disable" supported`, mode) } + // Set Server Name Indication (SNI), if enabled by connection parameters. + // By default SNI is on, any value which is not starting with "1" disables + // SNI -- that is the same check vanilla libpq uses. + if sslsni := o["sslsni"]; sslsni == "" || strings.HasPrefix(sslsni, "1") { + // RFC 6066 asks to not set SNI if the host is a literal IP address (IPv4 + // or IPv6). This check is coded already crypto.tls.hostnameInSNI, so + // just always set ServerName here and let crypto/tls do the filtering. + tlsConf.ServerName = o["host"] + } + err := sslClientCertificates(&tlsConf, o) if err != nil { return nil, err diff --git a/vendor/github.com/lib/pq/ssl_permissions.go b/vendor/github.com/lib/pq/ssl_permissions.go index 3b7c3a2a..d587f102 100644 --- a/vendor/github.com/lib/pq/ssl_permissions.go +++ b/vendor/github.com/lib/pq/ssl_permissions.go @@ -1,8 +1,30 @@ +//go:build !windows // +build !windows package pq -import "os" +import ( + "errors" + "os" + "syscall" +) + +const ( + rootUserID = uint32(0) + + // The maximum permissions that a private key file owned by a regular user + // is allowed to have. This translates to u=rw. + maxUserOwnedKeyPermissions os.FileMode = 0600 + + // The maximum permissions that a private key file owned by root is allowed + // to have. This translates to u=rw,g=r. + maxRootOwnedKeyPermissions os.FileMode = 0640 +) + +var ( + errSSLKeyHasUnacceptableUserPermissions = errors.New("permissions for files not owned by root should be u=rw (0600) or less") + errSSLKeyHasUnacceptableRootPermissions = errors.New("permissions for root owned files should be u=rw,g=r (0640) or less") +) // sslKeyPermissions checks the permissions on user-supplied ssl key files. // The key file should have very little access. @@ -13,8 +35,59 @@ func sslKeyPermissions(sslkey string) error { if err != nil { return err } - if info.Mode().Perm()&0077 != 0 { - return ErrSSLKeyHasWorldPermissions + + err = hasCorrectPermissions(info) + + // return ErrSSLKeyHasWorldPermissions for backwards compatability with + // existing code. + if err == errSSLKeyHasUnacceptableUserPermissions || err == errSSLKeyHasUnacceptableRootPermissions { + err = ErrSSLKeyHasWorldPermissions } - return nil + return err +} + +// hasCorrectPermissions checks the file info (and the unix-specific stat_t +// output) to verify that the permissions on the file are correct. +// +// If the file is owned by the same user the process is running as, +// the file should only have 0600 (u=rw). If the file is owned by root, +// and the group matches the group that the process is running in, the +// permissions cannot be more than 0640 (u=rw,g=r). The file should +// never have world permissions. +// +// Returns an error when the permission check fails. +func hasCorrectPermissions(info os.FileInfo) error { + // if file's permission matches 0600, allow access. + userPermissionMask := (os.FileMode(0777) ^ maxUserOwnedKeyPermissions) + + // regardless of if we're running as root or not, 0600 is acceptable, + // so we return if we match the regular user permission mask. + if info.Mode().Perm()&userPermissionMask == 0 { + return nil + } + + // We need to pull the Unix file information to get the file's owner. + // If we can't access it, there's some sort of operating system level error + // and we should fail rather than attempting to use faulty information. + sysInfo := info.Sys() + if sysInfo == nil { + return ErrSSLKeyUnknownOwnership + } + + unixStat, ok := sysInfo.(*syscall.Stat_t) + if !ok { + return ErrSSLKeyUnknownOwnership + } + + // if the file is owned by root, we allow 0640 (u=rw,g=r) to match what + // Postgres does. + if unixStat.Uid == rootUserID { + rootPermissionMask := (os.FileMode(0777) ^ maxRootOwnedKeyPermissions) + if info.Mode().Perm()&rootPermissionMask != 0 { + return errSSLKeyHasUnacceptableRootPermissions + } + return nil + } + + return errSSLKeyHasUnacceptableUserPermissions } diff --git a/vendor/github.com/lib/pq/ssl_windows.go b/vendor/github.com/lib/pq/ssl_windows.go index 5d2c763c..73663c8f 100644 --- a/vendor/github.com/lib/pq/ssl_windows.go +++ b/vendor/github.com/lib/pq/ssl_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package pq diff --git a/vendor/github.com/lib/pq/user_other.go b/vendor/github.com/lib/pq/user_other.go index f1c33134..3dae8f55 100644 --- a/vendor/github.com/lib/pq/user_other.go +++ b/vendor/github.com/lib/pq/user_other.go @@ -1,6 +1,7 @@ // Package pq is a pure Go Postgres driver for the database/sql package. -// +build js android hurd illumos zos +//go:build js || android || hurd || zos +// +build js android hurd zos package pq diff --git a/vendor/github.com/lib/pq/user_posix.go b/vendor/github.com/lib/pq/user_posix.go index a5101920..5f2d439b 100644 --- a/vendor/github.com/lib/pq/user_posix.go +++ b/vendor/github.com/lib/pq/user_posix.go @@ -1,6 +1,7 @@ // Package pq is a pure Go Postgres driver for the database/sql package. -// +build aix darwin dragonfly freebsd linux nacl netbsd openbsd plan9 solaris rumprun +//go:build aix || darwin || dragonfly || freebsd || (linux && !android) || nacl || netbsd || openbsd || plan9 || solaris || rumprun || illumos +// +build aix darwin dragonfly freebsd linux,!android nacl netbsd openbsd plan9 solaris rumprun illumos package pq diff --git a/vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md b/vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md index 1d4b2443..61b8cd35 100644 --- a/vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md +++ b/vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md @@ -10,6 +10,7 @@ Third-party patches are essential for keeping bluemonday secure and offering the 1. Do not vendor dependencies. As a security package, were we to vendor dependencies the projects that then vendor bluemonday may not receive the latest security updates to the dependencies. By not vendoring dependencies the project that implements bluemonday will vendor the latest version of any dependent packages. Vendoring is a project problem, not a package problem. bluemonday will be tested against the latest version of dependencies periodically and during any PR/merge. 2. I do not care about spelling mistakes or whitespace and I do not believe that you should either. PRs therefore must be functional in their nature or be substantial and impactful if documentation or examples. +3. This module does not participate in hacktober, please make your contributions meaningful. ## Submitting an Issue @@ -45,8 +46,6 @@ We haven't gone for the formal "Sign a Contributor Licence Agreement" thing that But we do need to know that we can accept and merge your contributions, so for now the act of contributing a pull request should be considered equivalent to agreeing to a contributor licence agreement, specifically: -You accept that the act of submitting code to the bluemonday project is to grant a copyright licence to the project that is perpetual, worldwide, non-exclusive, no-charge, royalty free and irrevocable. - -You accept that all who comply with the licence of the project (BSD 3-clause) are permitted to use your contributions to the project. - -You accept, and by submitting code do declare, that you have the legal right to grant such a licence to the project and that each of the contributions is your own original creation. +* You accept that the act of submitting code to the bluemonday project is to grant a copyright licence to the project that is perpetual, worldwide, non-exclusive, no-charge, royalty free and irrevocable. +* You accept that all who comply with the licence of the project (BSD 3-clause) are permitted to use your contributions to the project. +* You accept, and by submitting code do declare, that you have the legal right to grant such a licence to the project and that each of the contributions is your own original creation. diff --git a/vendor/github.com/microcosm-cc/bluemonday/LICENSE.md b/vendor/github.com/microcosm-cc/bluemonday/LICENSE.md index f822458e..2e6c493b 100644 --- a/vendor/github.com/microcosm-cc/bluemonday/LICENSE.md +++ b/vendor/github.com/microcosm-cc/bluemonday/LICENSE.md @@ -1,3 +1,6 @@ +SPDX short identifier: BSD-3-Clause +https://opensource.org/licenses/BSD-3-Clause + Copyright (c) 2014, David Kitchen All rights reserved. diff --git a/vendor/github.com/microcosm-cc/bluemonday/Makefile b/vendor/github.com/microcosm-cc/bluemonday/Makefile index dcd042a7..97e9541d 100644 --- a/vendor/github.com/microcosm-cc/bluemonday/Makefile +++ b/vendor/github.com/microcosm-cc/bluemonday/Makefile @@ -6,7 +6,7 @@ # fmt-check: Check if the source files are formated # build: Builds the code locally # vet: Vets the code -# lint: Runs lint over the code (you do not need to fix everything) +# staticcheck: Runs staticcheck over the code # test: Runs the tests # cover: Gives you the URL to a nice test coverage report # @@ -33,8 +33,8 @@ build: vet: @go vet -lint: - @golint *.go +staticcheck: + @staticcheck ./... test: @go test -v ./... @@ -42,7 +42,7 @@ test: cover: COVERAGE_FILE := coverage.out cover: @go test -coverprofile=$(COVERAGE_FILE) && \ - cover -html=$(COVERAGE_FILE) && rm $(COVERAGE_FILE) + go tool cover -html=$(COVERAGE_FILE) && rm $(COVERAGE_FILE) install: @go install ./... diff --git a/vendor/github.com/microcosm-cc/bluemonday/README.md b/vendor/github.com/microcosm-cc/bluemonday/README.md index d20debf0..8e658fea 100644 --- a/vendor/github.com/microcosm-cc/bluemonday/README.md +++ b/vendor/github.com/microcosm-cc/bluemonday/README.md @@ -1,4 +1,4 @@ -# bluemonday [![Build Status](https://travis-ci.org/microcosm-cc/bluemonday.svg?branch=master)](https://travis-ci.org/microcosm-cc/bluemonday) [![GoDoc](https://godoc.org/github.com/microcosm-cc/bluemonday?status.png)](https://godoc.org/github.com/microcosm-cc/bluemonday) [![Sourcegraph](https://sourcegraph.com/github.com/microcosm-cc/bluemonday/-/badge.svg)](https://sourcegraph.com/github.com/microcosm-cc/bluemonday?badge) +# bluemonday [![GoDoc](https://godoc.org/github.com/microcosm-cc/bluemonday?status.png)](https://godoc.org/github.com/microcosm-cc/bluemonday) [![Sourcegraph](https://sourcegraph.com/github.com/microcosm-cc/bluemonday/-/badge.svg)](https://sourcegraph.com/github.com/microcosm-cc/bluemonday?badge) bluemonday is a HTML sanitizer implemented in Go. It is fast and highly configurable. diff --git a/vendor/github.com/microcosm-cc/bluemonday/css/handlers.go b/vendor/github.com/microcosm-cc/bluemonday/css/handlers.go index 200a6729..f8b8b61a 100644 --- a/vendor/github.com/microcosm-cc/bluemonday/css/handlers.go +++ b/vendor/github.com/microcosm-cc/bluemonday/css/handlers.go @@ -280,6 +280,49 @@ var ( "slategray", "slategrey", "snow", "springgreen", "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", "whitesmoke", "yellow", "yellowgreen"} + + Alpha = regexp.MustCompile(`^[a-z]+$`) + Blur = regexp.MustCompile(`^blur\([0-9]+px\)$`) + BrightnessCont = regexp.MustCompile(`^(brightness|contrast)\([0-9]+\%\)$`) + Count = regexp.MustCompile(`^[0-9]+[\.]?[0-9]*$`) + CubicBezier = regexp.MustCompile(`^cubic-bezier\(([ ]*(0(.[0-9]+)?|1(.0)?),){3}[ ]*(0(.[0-9]+)?|1)\)$`) + Digits = regexp.MustCompile(`^digits [2-4]$`) + DropShadow = regexp.MustCompile(`drop-shadow\(([-]?[0-9]+px) ([-]?[0-9]+px)( [-]?[0-9]+px)?( ([-]?[0-9]+px))?`) + Font = regexp.MustCompile(`^('[a-z \-]+'|[a-z \-]+)$`) + Grayscale = regexp.MustCompile(`^grayscale\(([0-9]{1,2}|100)%\)$`) + GridTemplateAreas = regexp.MustCompile(`^['"]?[a-z ]+['"]?$`) + HexRGB = regexp.MustCompile(`^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$`) + HSL = regexp.MustCompile(`^hsl\([ ]*([012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%\)$`) + HSLA = regexp.MustCompile(`^hsla\(([ ]*[012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%,[ ]*(1|1\.0|0|(0\.[0-9]+))\)$`) + HueRotate = regexp.MustCompile(`^hue-rotate\(([12]?[0-9]{1,2}|3[0-5][0-9]|360)?\)$`) + Invert = regexp.MustCompile(`^invert\(([0-9]{1,2}|100)%\)$`) + Length = regexp.MustCompile(`^[\-]?([0-9]+|[0-9]*[\.][0-9]+)(%|cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|deg|rad|turn)?$`) + Matrix = regexp.MustCompile(`^matrix\(([ ]*[0-9]+[\.]?[0-9]*,){5}([ ]*[0-9]+[\.]?[0-9]*)\)$`) + Matrix3D = regexp.MustCompile(`^matrix3d\(([ ]*[0-9]+[\.]?[0-9]*,){15}([ ]*[0-9]+[\.]?[0-9]*)\)$`) + NegTime = regexp.MustCompile(`^[\-]?[0-9]+[\.]?[0-9]*(s|ms)?$`) + Numeric = regexp.MustCompile(`^[0-9]+$`) + NumericDecimal = regexp.MustCompile(`^[0-9\.]+$`) + Opactiy = regexp.MustCompile(`^opacity\(([0-9]{1,2}|100)%\)$`) + Perspective = regexp.MustCompile(`perspective\(`) + Position = regexp.MustCompile(`^[\-]*[0-9]+[cm|mm|in|px|pt|pc\%]* [[\-]*[0-9]+[cm|mm|in|px|pt|pc\%]*]*$`) + Opacity = regexp.MustCompile(`^(0[.]?[0-9]*)|(1.0)$`) + QuotedAlpha = regexp.MustCompile(`^["'][a-z]+["']$`) + Quotes = regexp.MustCompile(`^([ ]*["'][\x{0022}\x{0027}\x{2039}\x{2039}\x{203A}\x{00AB}\x{00BB}\x{2018}\x{2019}\x{201C}-\x{201E}]["'] ["'][\x{0022}\x{0027}\x{2039}\x{2039}\x{203A}\x{00AB}\x{00BB}\x{2018}\x{2019}\x{201C}-\x{201E}]["'])+$`) + Rect = regexp.MustCompile(`^rect\([0-9]+px,[ ]*[0-9]+px,[ ]*[0-9]+px,[ ]*[0-9]+px\)$`) + RGB = regexp.MustCompile(`^rgb\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){2}([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))))\)$`) + RGBA = regexp.MustCompile(`^rgba\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){3}[ ]*(1(\.0)?|0|(0\.[0-9]+))\)$`) + Rotate = regexp.MustCompile(`^rotate(x|y|z)?\(([12]?|3[0-5][0-9]|360)\)$`) + Rotate3D = regexp.MustCompile(`^rotate3d\(([ ]?(1(\.0)?|0\.[0-9]+),){3}([12]?|3[0-5][0-9]|360)\)$`) + Saturate = regexp.MustCompile(`^saturate\([0-9]+%\)$`) + Sepia = regexp.MustCompile(`^sepia\(([0-9]{1,2}|100)%\)$`) + Skew = regexp.MustCompile(`skew(x|y)?\(`) + Span = regexp.MustCompile(`^span [0-9]+$`) + Steps = regexp.MustCompile(`^steps\([ ]*[0-9]+([ ]*,[ ]*(start|end)?)\)$`) + Time = regexp.MustCompile(`^[0-9]+[\.]?[0-9]*(s|ms)?$`) + TransitionProp = regexp.MustCompile(`^([a-zA-Z]+,[ ]?)*[a-zA-Z]+$`) + TranslateScale = regexp.MustCompile(`(translate|translate3d|translatex|translatey|translatez|scale|scale3d|scalex|scaley|scalez)\(`) + URL = regexp.MustCompile(`^url\([\"\']?((https|http)[a-z0-9\.\\/_:]+[\"\']?)\)$`) + ZIndex = regexp.MustCompile(`^[\-]?[0-9]+$`) ) func multiSplit(value string, seps ...string) []string { @@ -323,10 +366,11 @@ func in(value []string, arr []string) bool { func splitValues(value string) []string { values := strings.Split(value, ",") + newValues := []string{} for _, strippedValue := range values { - strippedValue = strings.ToLower(strings.TrimSpace(strippedValue)) + newValues = append(newValues, strings.ToLower(strings.TrimSpace(strippedValue))) } - return values + return newValues } func GetDefaultHandler(attr string) func(string) bool { @@ -388,9 +432,7 @@ func AnimationHandler(value string) bool { } func AnimationDelayHandler(value string) bool { - reg := regexp.MustCompile(`[\-]?[0-9]+[\.]?[0-9]*[s|ms]?`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if NegTime.MatchString(value) { return true } values := []string{"initial", "inherit"} @@ -405,9 +447,7 @@ func AnimationDirectionHandler(value string) bool { } func AnimationDurationHandler(value string) bool { - reg := regexp.MustCompile(`[0-9]+[\.]?[0-9]*[s|ms]?`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Time.MatchString(value) { return true } values := []string{"initial", "inherit"} @@ -422,9 +462,7 @@ func AnimationFillModeHandler(value string) bool { } func AnimationIterationCountHandler(value string) bool { - reg := regexp.MustCompile(`[0-9]+[\.]?[0-9]*`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Count.MatchString(value) { return true } values := []string{"infinite", "initial", "inherit"} @@ -433,9 +471,7 @@ func AnimationIterationCountHandler(value string) bool { } func AnimationNameHandler(value string) bool { - reg := regexp.MustCompile(`[a-z]+`) - reg.Longest() - return reg.FindString(value) == value && value != "" + return Alpha.MatchString(value) } func AnimationPlayStateHandler(value string) bool { @@ -450,14 +486,10 @@ func TimingFunctionHandler(value string) bool { if in(splitVals, values) { return true } - reg := regexp.MustCompile(`cubic-bezier\(([ ]*(0(.[0-9]+)?|1(.0)?),){3}[ ]*(0(.[0-9]+)?|1)\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if CubicBezier.MatchString(value) { return true } - reg = regexp.MustCompile(`steps\([ ]*[0-9]+([ ]*,[ ]*(start|end)?)\)`) - reg.Longest() - return reg.FindString(value) == value && value != "" + return Steps.MatchString(value) } func BackfaceVisibilityHandler(value string) bool { @@ -518,9 +550,7 @@ func ImageHandler(value string) bool { if in(splitVals, values) { return true } - reg := regexp.MustCompile(`url\([\"\']?((https|http)[a-z0-9\.\\/_:]+[\"\']?)\)`) - reg.Longest() - return reg.FindString(value) == value && value != "" + return URL.MatchString(value) } func BackgroundOriginHandler(value string) bool { @@ -535,12 +565,7 @@ func BackgroundPositionHandler(value string) bool { if in(splitVals, values) { return true } - reg := regexp.MustCompile(`[\-]*[0-9]+[cm|mm|in|px|pt|pc\%]* [[\-]*[0-9]+[cm|mm|in|px|pt|pc\%]*]*`) - reg.Longest() - if reg.FindString(value) == value && value != "" { - return true - } - return false + return Position.MatchString(value) } func BackgroundRepeatHandler(value string) bool { @@ -816,31 +841,19 @@ func CaretColorHandler(value string) bool { if in(splitVals, colorValues) { return true } - reg := regexp.MustCompile(`#[0-9abcdef]{6}`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if HexRGB.MatchString(value) { return true } - reg = regexp.MustCompile(`rgb\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){2}([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))))\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if RGB.MatchString(value) { return true } - reg = regexp.MustCompile(`rgba\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){3}[ ]*(1(\.0)?|0|(0\.[0-9]+))\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if RGBA.MatchString(value) { return true } - reg = regexp.MustCompile(`hsl\([ ]*([012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%\)`) - if reg.FindString(value) == value && value != "" { + if HSL.MatchString(value) { return true } - reg = regexp.MustCompile(`hsla\(([ ]*[012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%,[ ]*(1|1\.0|0|(0\.[0-9]+))\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { - return true - } - return false + return HSLA.MatchString(value) } func ClearHandler(value string) bool { @@ -850,9 +863,7 @@ func ClearHandler(value string) bool { } func ClipHandler(value string) bool { - reg := regexp.MustCompile(`rect\([0-9]+px,[ ]*[0-9]+px,[ ]*[0-9]+px,[ ]*[0-9]+px\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Rect.MatchString(value) { return true } values := []string{"auto", "initial", "inherit"} @@ -865,38 +876,23 @@ func ColorHandler(value string) bool { if in(splitVals, colorValues) { return true } - reg := regexp.MustCompile(`#[0-9abcdef]{6}`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if HexRGB.MatchString(value) { return true } - reg = regexp.MustCompile(`rgb\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){2}([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))))\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if RGB.MatchString(value) { return true } - reg = regexp.MustCompile(`rgba\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){3}[ ]*(1(\.0)?|0|(0\.[0-9]+))\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if RGBA.MatchString(value) { return true } - reg = regexp.MustCompile(`hsl\([ ]*([012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if HSL.MatchString(value) { return true } - reg = regexp.MustCompile(`hsla\(([ ]*[012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%,[ ]*(1|1\.0|0|(0\.[0-9]+))\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { - return true - } - return false + return HSLA.MatchString(value) } func ColumnCountHandler(value string) bool { - reg := regexp.MustCompile(`[0-9]+`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Numeric.MatchString(value) { return true } values := []string{"auto", "initial", "inherit"} @@ -1000,54 +996,35 @@ func FilterHandler(value string) bool { if in(splitVals, values) { return true } - reg := regexp.MustCompile(`blur\([0-9]+px\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Blur.MatchString(value) { return true } - reg = regexp.MustCompile(`(brightness|contrast)\([0-9]+\%\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if BrightnessCont.MatchString(value) { return true } - reg = regexp.MustCompile(`drop-shadow\(([-]?[0-9]+px) ([-]?[0-9]+px)( [-]?[0-9]+px)?( ([-]?[0-9]+px))?`) - reg.Longest() - colorValue := strings.TrimSuffix(string(reg.ReplaceAll([]byte(value), []byte{})), ")") + if DropShadow.MatchString(value) { + return true + } + colorValue := strings.TrimSuffix(string(DropShadow.ReplaceAll([]byte(value), []byte{})), ")") if ColorHandler(colorValue) { return true } - reg = regexp.MustCompile(`grayscale\(([0-9]{1,2}|100)%\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Grayscale.MatchString(value) { return true } - reg = regexp.MustCompile(`hue-rotate\(([12]?[0-9]{1,2}|3[0-5][0-9]|360)?\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if HueRotate.MatchString(value) { return true } - reg = regexp.MustCompile(`invert\(([0-9]{1,2}|100)%\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Invert.MatchString(value) { return true } - reg = regexp.MustCompile(`opacity\(([0-9]{1,2}|100)%\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Opacity.MatchString(value) { return true } - reg = regexp.MustCompile(`saturate\([0-9]+%\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Saturate.MatchString(value) { return true } - reg = regexp.MustCompile(`sepia\(([0-9]{1,2}|100)%\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { - return true - } - //Not allowing URLs - return false + return Sepia.MatchString(value) } func FlexHandler(value string) bool { @@ -1092,9 +1069,7 @@ func FlexFlowHandler(value string) bool { } func FlexGrowHandler(value string) bool { - reg := regexp.MustCompile(`[0-9\.]+`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if NumericDecimal.MatchString(value) { return true } splitVals := strings.Split(value, ";") @@ -1144,11 +1119,9 @@ func FontFamilyHandler(value string) bool { if in(splitVals, values) { return true } - reg := regexp.MustCompile(`('[a-z \-]+'|[a-z \-]+)`) - reg.Longest() for _, i := range splitVals { i = strings.TrimSpace(i) - if reg.FindString(i) != i { + if Font.FindString(i) != i { return false } } @@ -1162,9 +1135,7 @@ func FontKerningHandler(value string) bool { } func FontLanguageOverrideHandler(value string) bool { - reg := regexp.MustCompile(`[a-z]+`) - reg.Longest() - return reg.FindString(value) == value && value != "" + return Alpha.MatchString(value) } func FontSizeHandler(value string) bool { @@ -1177,9 +1148,7 @@ func FontSizeHandler(value string) bool { } func FontSizeAdjustHandler(value string) bool { - reg := regexp.MustCompile(`[0-9]+[\.]?[0-9]*`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Count.MatchString(value) { return true } values := []string{"auto", "initial", "inherit"} @@ -1298,9 +1267,7 @@ func GridColumnGapHandler(value string) bool { } func LengthHandler(value string) bool { - reg := regexp.MustCompile(`[\-]?[0-9]+[\.]?[0-9]*(%|cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|deg|rad|turn)?`) - reg.Longest() - return reg.FindString(value) == value && value != "" + return Length.MatchString(value) } func LineBreakHandler(value string) bool { @@ -1310,12 +1277,10 @@ func LineBreakHandler(value string) bool { } func GridAxisStartEndHandler(value string) bool { - reg := regexp.MustCompile(`[0-9]+`) - if reg.FindString(value) == value && value != "" { + if Numeric.MatchString(value) { return true } - reg = regexp.MustCompile(`span [0-9]+`) - if reg.FindString(value) == value && value != "" { + if Span.MatchString(value) { return true } values := []string{"auto"} @@ -1366,9 +1331,7 @@ func GridTemplateAreasHandler(value string) bool { if in([]string{value}, values) { return true } - reg := regexp.MustCompile(`['"]?[a-z ]+['"]?`) - reg.Longest() - return reg.FindString(value) == value && value != "" + return GridTemplateAreas.MatchString(value) } func GridTemplateColumnsHandler(value string) bool { @@ -1551,9 +1514,7 @@ func ObjectPositionHandler(value string) bool { } func OpacityHandler(value string) bool { - reg := regexp.MustCompile("(0[.]?[0-9]*)|(1.0)") - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Opacity.MatchString(value) { return true } values := []string{"initial", "inherit"} @@ -1562,9 +1523,7 @@ func OpacityHandler(value string) bool { } func OrderHandler(value string) bool { - reg := regexp.MustCompile("[0-9]+") - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Numeric.MatchString(value) { return true } values := []string{"initial", "inherit"} @@ -1629,9 +1588,7 @@ func OverflowWrapHandler(value string) bool { } func OrphansHandler(value string) bool { - reg := regexp.MustCompile(`[0-9]+`) - reg.Longest() - return reg.FindString(value) == value && value != "" + return Numeric.MatchString(value) } func PaddingHandler(value string) bool { @@ -1716,9 +1673,7 @@ func QuotesHandler(value string) bool { if in(splitVals, values) { return true } - reg := regexp.MustCompile(`([ ]*["'][\x{0022}\x{0027}\x{2039}\x{2039}\x{203A}\x{00AB}\x{00BB}\x{2018}\x{2019}\x{201C}-\x{201E}]["'] ["'][\x{0022}\x{0027}\x{2039}\x{2039}\x{203A}\x{00AB}\x{00BB}\x{2018}\x{2019}\x{201C}-\x{201E}]["'])+`) - reg.Longest() - return reg.FindString(value) == value && value != "" + return Quotes.MatchString(value) } func ResizeHandler(value string) bool { @@ -1766,9 +1721,7 @@ func TextCombineUprightHandler(value string) bool { if in(splitVals, values) { return true } - reg := regexp.MustCompile(`digits [2-4]`) - reg.Longest() - return reg.FindString(value) == value && value != "" + return Digits.MatchString(value) } func TextDecorationHandler(value string) bool { @@ -1813,9 +1766,7 @@ func TextJustifyHandler(value string) bool { } func TextOverflowHandler(value string) bool { - reg := regexp.MustCompile("[\"'][a-z]+[\"']") - reg.Longest() - if reg.FindString(value) == value && value != "" { + if QuotedAlpha.MatchString(value) { return true } values := []string{"clip", "ellipsis", "initial", "inherit"} @@ -1868,18 +1819,13 @@ func TransformHandler(value string) bool { if in([]string{value}, values) { return true } - reg := regexp.MustCompile(`matrix\(([ ]*[0-9]+[\.]?[0-9]*,){5}([ ]*[0-9]+[\.]?[0-9]*)\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Matrix.MatchString(value) { return true } - reg = regexp.MustCompile(`matrix3d\(([ ]*[0-9]+[\.]?[0-9]*,){15}([ ]*[0-9]+[\.]?[0-9]*)\)`) - if reg.FindString(value) == value && value != "" { + if Matrix3D.MatchString(value) { return true } - reg = regexp.MustCompile(`(translate|translate3d|translatex|translatey|translatez|scale|scale3d|scalex|scaley|scalez)\(`) - reg.Longest() - subValue := string(reg.ReplaceAll([]byte(value), []byte{})) + subValue := string(TranslateScale.ReplaceAll([]byte(value), []byte{})) trimValue := strings.Split(strings.TrimSuffix(subValue, ")"), ",") valid := true for _, i := range trimValue { @@ -1891,19 +1837,13 @@ func TransformHandler(value string) bool { if valid && trimValue != nil { return true } - reg = regexp.MustCompile(`rotate(x|y|z)?\(([12]?|3[0-5][0-9]|360)\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Rotate.MatchString(value) { return true } - reg = regexp.MustCompile(`rotate3d\(([ ]?(1(\.0)?|0\.[0-9]+),){3}([12]?|3[0-5][0-9]|360)\)`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Rotate3D.MatchString(value) { return true } - reg = regexp.MustCompile(`skew(x|y)?\(`) - reg.Longest() - subValue = string(reg.ReplaceAll([]byte(value), []byte{})) + subValue = string(Skew.ReplaceAll([]byte(value), []byte{})) subValue = strings.TrimSuffix(subValue, ")") trimValue = strings.Split(subValue, ",") valid = true @@ -1916,9 +1856,7 @@ func TransformHandler(value string) bool { if valid { return true } - reg = regexp.MustCompile(`perspective\(`) - reg.Longest() - subValue = string(reg.ReplaceAll([]byte(value), []byte{})) + subValue = string(Perspective.ReplaceAll([]byte(value), []byte{})) subValue = strings.TrimSuffix(subValue, ")") return LengthHandler(subValue) } @@ -1973,9 +1911,7 @@ func TransitionHandler(value string) bool { } func TransitionDelayHandler(value string) bool { - reg := regexp.MustCompile("[0-9]+[.]?[0-9]*(s|ms)?") - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Time.MatchString(value) { return true } values := []string{"initial", "inherit"} @@ -1984,9 +1920,7 @@ func TransitionDelayHandler(value string) bool { } func TransitionDurationHandler(value string) bool { - reg := regexp.MustCompile("[0-9]+[.]?[0-9]*(s|ms)?") - reg.Longest() - if reg.FindString(value) == value && value != "" { + if Time.MatchString(value) { return true } values := []string{"initial", "inherit"} @@ -1995,9 +1929,7 @@ func TransitionDurationHandler(value string) bool { } func TransitionPropertyHandler(value string) bool { - reg := regexp.MustCompile("([a-zA-Z]+,[ ]?)*[a-zA-Z]+") - reg.Longest() - if reg.FindString(value) == value && value != "" { + if TransitionProp.MatchString(value) { return true } values := []string{"none", "all", "initial", "inherit"} @@ -2075,9 +2007,7 @@ func WritingModeHandler(value string) bool { } func ZIndexHandler(value string) bool { - reg := regexp.MustCompile(`[\-]?[0-9]+`) - reg.Longest() - if reg.FindString(value) == value && value != "" { + if ZIndex.MatchString(value) { return true } values := []string{"auto", "initial", "inherit"} diff --git a/vendor/github.com/microcosm-cc/bluemonday/doc.go b/vendor/github.com/microcosm-cc/bluemonday/doc.go index ba2d775a..d95e8a9d 100644 --- a/vendor/github.com/microcosm-cc/bluemonday/doc.go +++ b/vendor/github.com/microcosm-cc/bluemonday/doc.go @@ -35,31 +35,31 @@ the allowlist will be stripped. The default bluemonday.UGCPolicy().Sanitize() turns this: - Hello World + Hello World Into the more harmless: - Hello World + Hello World And it turns this: - XSS + XSS Into this: - XSS + XSS Whilst still allowing this: - - - + + + To pass through mostly unaltered (it gained a rel="nofollow"): - - - + + + The primary purpose of bluemonday is to take potentially unsafe user generated content (from things like Markdown, HTML WYSIWYG tools, etc) and make it safe @@ -95,10 +95,10 @@ attributes are considered safe for your scenario. OWASP provide an XSS prevention cheat sheet ( https://www.google.com/search?q=xss+prevention+cheat+sheet ) to help explain the risks, but essentially: - 1. Avoid allowing anything other than plain HTML elements - 2. Avoid allowing `script`, `style`, `iframe`, `object`, `embed`, `base` - elements - 3. Avoid allowing anything other than plain HTML elements with simple - values that you can match to a regexp + 1. Avoid allowing anything other than plain HTML elements + 2. Avoid allowing `script`, `style`, `iframe`, `object`, `embed`, `base` + elements + 3. Avoid allowing anything other than plain HTML elements with simple + values that you can match to a regexp */ package bluemonday diff --git a/vendor/github.com/microcosm-cc/bluemonday/helpers.go b/vendor/github.com/microcosm-cc/bluemonday/helpers.go index 4e31fb6f..aa0b7b92 100644 --- a/vendor/github.com/microcosm-cc/bluemonday/helpers.go +++ b/vendor/github.com/microcosm-cc/bluemonday/helpers.go @@ -117,7 +117,7 @@ var ( // This is not exported as it's not useful by itself, and only has value // within the AllowDataURIImages func dataURIImagePrefix = regexp.MustCompile( - `^image/(gif|jpeg|png|webp);base64,`, + `^image/(gif|jpeg|png|svg\+xml|webp);base64,`, ) ) @@ -193,10 +193,11 @@ func (p *Policy) AllowImages() { // http://en.wikipedia.org/wiki/Data_URI_scheme // // Images must have a mimetype matching: -// image/gif -// image/jpeg -// image/png -// image/webp +// +// image/gif +// image/jpeg +// image/png +// image/webp // // NOTE: There is a potential security risk to allowing data URIs and you should // only permit them on content you already trust. @@ -221,11 +222,7 @@ func (p *Policy) AllowDataURIImages() { } _, err := base64.StdEncoding.DecodeString(url.Opaque[len(matched):]) - if err != nil { - return false - } - - return true + return err == nil }, ) } diff --git a/vendor/github.com/microcosm-cc/bluemonday/policy.go b/vendor/github.com/microcosm-cc/bluemonday/policy.go index 8a08163d..b4f09879 100644 --- a/vendor/github.com/microcosm-cc/bluemonday/policy.go +++ b/vendor/github.com/microcosm-cc/bluemonday/policy.go @@ -117,6 +117,19 @@ type Policy struct { // returning true are allowed. allowURLSchemes map[string][]urlPolicy + // These regexps are used to match allowed URL schemes, for example + // if one would want to allow all URL schemes, they would add `.+`. + // However pay attention as this can lead to XSS being rendered thus + // defeating the purpose of using a HTML sanitizer. + // The regexps are only considered if a schema was not explicitly + // handled by `AllowURLSchemes` or `AllowURLSchemeWithCustomPolicy`. + allowURLSchemeRegexps []*regexp.Regexp + + // If srcRewriter is not nil, it is used to rewrite the src attribute + // of tags that download resources, such as and