From 82be8c0c89b9bd248e5f94b89732abaf6ec8ed7d Mon Sep 17 00:00:00 2001 From: nabbar Date: Thu, 23 Nov 2023 01:01:46 +0100 Subject: [PATCH] Pakcage AWS: - add subpackage to sign http.request with AWS V4 signature and parse response based on http.response and given model Other: - bump dependencies --- aws/aws_suite_test.go | 2 + aws/configAws/models.go | 4 + aws/configCustom/models.go | 4 + aws/http/request.go | 78 +++++++++++++++++++ aws/http/response.go | 156 +++++++++++++++++++++++++++++++++++++ aws/interface.go | 1 + go.mod | 10 ++- 7 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 aws/http/request.go create mode 100644 aws/http/response.go diff --git a/aws/aws_suite_test.go b/aws/aws_suite_test.go index 32d536f0..86a8badf 100644 --- a/aws/aws_suite_test.go +++ b/aws/aws_suite_test.go @@ -239,6 +239,8 @@ func WaitMinio(host string) bool { } }() + time.Sleep(5 * time.Second) + return err == nil } diff --git a/aws/configAws/models.go b/aws/configAws/models.go index 0f8c6f0b..992c0b2a 100644 --- a/aws/configAws/models.go +++ b/aws/configAws/models.go @@ -80,6 +80,10 @@ func (c *awsModel) GetAccessKey() string { return c.AccessKey } +func (c *awsModel) GetSecretKey() string { + return c.SecretKey +} + func (c *awsModel) SetCredentials(accessKey, secretKey string) { c.AccessKey = accessKey c.SecretKey = secretKey diff --git a/aws/configCustom/models.go b/aws/configCustom/models.go index f2b2f1fa..28d4a2fc 100644 --- a/aws/configCustom/models.go +++ b/aws/configCustom/models.go @@ -102,6 +102,10 @@ func (c *awsModel) GetAccessKey() string { return c.AccessKey } +func (c *awsModel) GetSecretKey() string { + return c.SecretKey +} + func (c *awsModel) SetCredentials(accessKey, secretKey string) { c.AccessKey = accessKey c.SecretKey = secretKey diff --git a/aws/http/request.go b/aws/http/request.go new file mode 100644 index 00000000..c5166029 --- /dev/null +++ b/aws/http/request.go @@ -0,0 +1,78 @@ +/* + * MIT License + * + * Copyright (c) 2020 Nicolas JUHEL + * + * 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. + * + */ + +package http + +import ( + "bytes" + "io" + "net/http" + "time" + + sdkcrd "github.com/aws/aws-sdk-go/aws/credentials" + sdksv4 "github.com/aws/aws-sdk-go/aws/signer/v4" +) + +func CopyReader(r io.Reader) (io.ReadSeekCloser, error) { + var tmp = bytes.NewBuffer(make([]byte, 0)) + + if _, err := io.Copy(tmp, r); err != nil { + return nil, err + } else { + return &readerCloser{bytes.NewReader(tmp.Bytes())}, nil + } +} + +func NewReader(p []byte) io.ReadSeekCloser { + return &readerCloser{bytes.NewReader(p)} +} + +type readerCloser struct { + io.ReadSeeker +} + +func (r *readerCloser) Close() error { + return nil +} + +func Request(req *http.Request, cfg Config, service string) error { + var ( + err error + sig = sdksv4.NewSigner(sdkcrd.NewStaticCredentials(cfg.GetAccessKey(), cfg.GetSecretKey(), "")) + ) + + if req.Body == nil { + _, err = sig.Sign(req, nil, service, cfg.GetRegion(), time.Now()) + } else if r, k := req.Body.(io.ReadSeekCloser); k { + _, err = sig.Sign(req, r, service, cfg.GetRegion(), time.Now()) + } else if r, err = CopyReader(req.Body); err != nil { + return err + } else { + req.Body = r + _, err = sig.Sign(req, r, service, cfg.GetRegion(), time.Now()) + } + + return err +} diff --git a/aws/http/response.go b/aws/http/response.go new file mode 100644 index 00000000..e97c636b --- /dev/null +++ b/aws/http/response.go @@ -0,0 +1,156 @@ +/* + * MIT License + * + * Copyright (c) 2020 Nicolas JUHEL + * + * 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. + * + */ + +package http + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "fmt" + "io" + "mime" + "net/http" +) + +var ( + ErrInvalidResponse = fmt.Errorf("invalid response") +) + +type Config interface { + GetRegion() string + GetAccessKey() string + GetSecretKey() string +} + +type ErrorResponse struct { + XMLName xml.Name `xml:"Error"` + Code string `xml:"Code" json:"code"` + Message string `xml:"Message" json:"message"` + RequestID string `xml:"RequestId" json:"requestId"` +} + +func (e ErrorResponse) Error() string { + return fmt.Sprintf("request ID %s occure an aws response error %s: %s", e.RequestID, e.Code, e.Message) +} + +type ErrorStatus struct { + Status string `xml:"Code" json:"code"` + Message string `xml:"Message" json:"message"` +} + +func (e ErrorStatus) Error() string { + return fmt.Sprintf("invalid response status code (%s): %s", e.Status, e.Message) +} + +func Response(rsp *http.Response, model any) error { + defer func() { + if rsp != nil && rsp.Body != nil { + _ = rsp.Body.Close() + } + }() + + var ( + err error + buf = bytes.NewBuffer(make([]byte, 0)) + cnj = mime.TypeByExtension(".json") + cnx = mime.TypeByExtension(".xml") + ) + + if rsp == nil { + return ErrInvalidResponse + } else if rsp.Body != nil { + if _, e := io.Copy(buf, rsp.Body); e != nil { + return e + } + } + + if tp := rsp.Header.Get("Content-Type"); tp == cnj { + err = responseJson(buf, model) + } else if tp != cnx { + err = responseXml(buf, model) + } else { + return ErrInvalidResponse + } + + if rsp.StatusCode < 200 || rsp.StatusCode >= 300 { + if err != nil { + return err + } else { + return &ErrorStatus{ + Status: rsp.Status, + Message: truncateBuf(buf), + } + } + } else if err != nil { + return err + } + + return nil +} + +func truncateBuf(buf *bytes.Buffer) string { + if buf.Len() > 255 { + return buf.String()[:255] + } else { + return buf.String() + } +} + +func responseJson(buf *bytes.Buffer, model any) error { + if e := json.Unmarshal(buf.Bytes(), model); e != nil { + return responseJsonError(buf) + } + + return nil +} + +func responseJsonError(buf *bytes.Buffer) error { + var err = ErrorResponse{} + + if e := json.Unmarshal(buf.Bytes(), &err); e != nil { + return e + } else { + return err + } +} + +func responseXml(buf *bytes.Buffer, model any) error { + if e := xml.Unmarshal(buf.Bytes(), model); e != nil { + return responseXmlError(buf) + } + + return nil +} + +func responseXmlError(buf *bytes.Buffer) error { + var err = ErrorResponse{} + + if e := xml.Unmarshal(buf.Bytes(), &err); e != nil { + return e + } else { + return err + } +} diff --git a/aws/interface.go b/aws/interface.go index e12b5518..c0f2dec7 100644 --- a/aws/interface.go +++ b/aws/interface.go @@ -50,6 +50,7 @@ type Config interface { Validate() error GetAccessKey() string + GetSecretKey() string SetCredentials(accessKey, secretKey string) ResetRegionEndpoint() RegisterRegionEndpoint(region string, endpoint *url.URL) error diff --git a/go.mod b/go.mod index fc6a0f60..08be0d27 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,15 @@ module github.com/nabbar/golib go 1.21 -toolchain go1.21.3 +toolchain go1.21.4 require ( + github.com/aws/aws-sdk-go v1.48.3 github.com/aws/aws-sdk-go-v2 v1.23.1 - github.com/aws/aws-sdk-go-v2/config v1.25.4 - github.com/aws/aws-sdk-go-v2/credentials v1.16.3 + github.com/aws/aws-sdk-go-v2/config v1.25.5 + github.com/aws/aws-sdk-go-v2/credentials v1.16.4 github.com/aws/aws-sdk-go-v2/service/iam v1.27.3 - github.com/aws/aws-sdk-go-v2/service/s3 v1.43.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.44.0 github.com/aws/smithy-go v1.17.0 github.com/bits-and-blooms/bitset v1.11.0 github.com/c-bata/go-prompt v0.2.6 @@ -146,6 +147,7 @@ require ( github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/juju/ratelimit v1.0.2 // indirect github.com/klauspost/compress v1.17.3 // indirect