From 9dfea3b9c30228a96b5c0d9c5108496acb8262d2 Mon Sep 17 00:00:00 2001 From: "ffauteux@bluecatnetworks.com" Date: Thu, 23 Nov 2023 17:06:51 -0500 Subject: [PATCH] EDG-13557: GET method --- README.md | 43 ++++++++++++++++++++++++++++++++----------- aws.go | 23 ++++++++++++++++++++++- local/local.go | 11 +++++++++++ s3/s3.go | 22 ++++++++++++++++++---- signer/signer.go | 6 +++++- 5 files changed, 88 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 8d5f9ff..3dc0e13 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # Traefik AWS Plugin -This is a [Traefik middleware plugin](https://plugins.traefik.io) which pushes data to and pulls data from Amazon Web Services (AWS) for a Traefik instance running in [Amazon Elastic Container Service (ECS)](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html). The following is currently supported: - -* [Amazon Simple Storage Service (S3)](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html): PUT -* [Amazon DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html): pending - +This is a [Traefik middleware plugin](https://plugins.traefik.io) which pushes data to and pulls data from Amazon Web Services (AWS) for a Traefik instance running in [Amazon Elastic Container Service (ECS)](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html). ## Configuration traefik.yml: @@ -30,7 +26,22 @@ Example labels for a given router: "traefik.http.routers.my-router.middlewares" : "my-aws" ``` -S3 example labels, `prefix` includes leading slash: +## Services + +### Local storage + +To store objects in a local directory, use the following labels (example): + +```text +"traefik.http.middlewares.my-aws.plugin.aws.type" : "local" +"traefik.http.middlewares.my-aws.plugin.aws.directory" : "aws-local-directory" +``` + +`GET` and `PUT` are supported. + +### S3 + +To store objects in [Amazon Simple Storage Service (S3)](https://docs.aws.amazon.com/AmazonS3/latest/userguide), use the following labels (example): ```text "traefik.http.middlewares.my-aws.plugin.aws.service" : "s3" @@ -40,12 +51,22 @@ S3 example labels, `prefix` includes leading slash: } ``` -Local directory example labels: +Note that `prefix` must include the leading slash. -```text -"traefik.http.middlewares.my-aws.plugin.aws.type" : "local" -"traefik.http.middlewares.my-aws.plugin.aws.directory" : "aws-local-directory" -``` +[PUT](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html) and [GET](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) are supported. + +When forwarding the request to S3, the plugin sets the following headers: + +* `Host` +* `Authorization` with the [AWS API request signature](https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html); the [ECS task IAM role credentials](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html) are used to sign the request +* `date`, if not defined +* `X-Amz-Content-Sha256` +* `X-Amz-Date` +* `x-amz-security-token` + +### DynamoDB + +[Amazon DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide) support is pending. ## Development diff --git a/aws.go b/aws.go index 8c69613..41a7c9a 100644 --- a/aws.go +++ b/aws.go @@ -13,6 +13,7 @@ import ( type Service interface { Put(name string, payload []byte, contentType string, rw http.ResponseWriter) ([]byte, error) + Get(name string, rw http.ResponseWriter) ([]byte, error) } type Config struct { @@ -39,6 +40,18 @@ type AwsPlugin struct { } func (plugin AwsPlugin) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + switch req.Method { + case "PUT": + plugin.put(rw, req) + case "GET": + plugin.get(rw, req) + default: + http.Error(rw, fmt.Sprintf("Method %s not implemented", req.Method), http.StatusNotImplemented) + } + plugin.next.ServeHTTP(rw, req) +} + +func (plugin AwsPlugin) put(rw http.ResponseWriter, req *http.Request) { payload, err := io.ReadAll(req.Body) if err != nil { rw.WriteHeader(http.StatusNotAcceptable) @@ -46,6 +59,15 @@ func (plugin AwsPlugin) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } resp, err := plugin.service.Put(req.URL.Path[1:], payload, req.Header.Get("Content-Type"), rw) + handleResponse(resp, err, rw) +} + +func (plugin *AwsPlugin) get(rw http.ResponseWriter, req *http.Request) { + resp, err := plugin.service.Get(req.URL.Path[1:], rw) + handleResponse(resp, err, rw) +} + +func handleResponse(resp []byte, err error, rw http.ResponseWriter) { if err != nil { rw.WriteHeader(http.StatusInternalServerError) http.Error(rw, fmt.Sprintf("Put error: %s", err.Error()), http.StatusInternalServerError) @@ -57,7 +79,6 @@ func (plugin AwsPlugin) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if err != nil { http.Error(rw, string(resp)+err.Error(), http.StatusBadGateway) } - plugin.next.ServeHTTP(rw, req) } func New(_ context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { diff --git a/local/local.go b/local/local.go index b06d524..f6441e7 100644 --- a/local/local.go +++ b/local/local.go @@ -32,3 +32,14 @@ func (local *Local) Put(name string, payload []byte, _ string, _ http.ResponseWr log.Debug(fmt.Sprintf("%q written", filePath)) return []byte(fmt.Sprintf("%q written", filePath)), nil } + +func (local *Local) Get(name string, _ http.ResponseWriter) ([]byte, error) { + filePath := fmt.Sprintf("%s/%s", local.directory, name) + payload, err := os.ReadFile(filePath) + if err != nil { + log.Error(err.Error()) + return nil, err + } + log.Debug(fmt.Sprintf("%q read", filePath)) + return payload, nil +} diff --git a/s3/s3.go b/s3/s3.go index 7df2fa9..419a3ee 100644 --- a/s3/s3.go +++ b/s3/s3.go @@ -35,9 +35,13 @@ func New(bucket, prefix, region string, timeoutSeconds int, creds *ecs.Credentia } } -func (s3 *S3) Put(name string, payload []byte, contentType string, rw http.ResponseWriter) ([]byte, error) { +func (s3 *S3) request(httpMethod string, name string, payload []byte, contentType string, rw http.ResponseWriter) ([]byte, error) { uri := s3.bucketUri + s3.prefix + "/" + name - req, err := http.NewRequest("PUT", uri, bytes.NewReader(payload)) + var payloadReader io.Reader = nil + if payload != nil { + payloadReader = bytes.NewReader(payload) + } + req, err := http.NewRequest(httpMethod, uri, payloadReader) if err != nil { log.Error(err.Error()) return nil, err @@ -46,13 +50,15 @@ func (s3 *S3) Put(name string, payload []byte, contentType string, rw http.Respo if cancel != nil { defer cancel() } - req.Header.Set("Content-Type", contentType) + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } req.Header.Set("Host", req.URL.Host) cr := signer.CreateCanonRequest(req, payload, *s3.crTemplate) req.Header.Set("Authorization", cr.AuthHeader()) resp, err := s3.client.Do(req.WithContext(ctx)) if err != nil { - log.Error(fmt.Sprintf("PUT %q failed, status: %q, error: %s", uri, resp.Status, err.Error())) + log.Error(fmt.Sprintf("%s %q failed, status: %q, error: %s", httpMethod, uri, resp.Status, err.Error())) return nil, err } if resp.StatusCode > 299 { @@ -66,6 +72,14 @@ func (s3 *S3) Put(name string, payload []byte, contentType string, rw http.Respo return response, nil } +func (s3 *S3) Put(name string, payload []byte, contentType string, rw http.ResponseWriter) ([]byte, error) { + return s3.request("PUT", name, payload, contentType, rw) +} + +func (s3 *S3) Get(name string, rw http.ResponseWriter) ([]byte, error) { + return s3.request("GET", name, nil, "", rw) +} + func copyHeader(dst, src http.Header) { for k, vv := range src { for _, v := range vv { diff --git a/signer/signer.go b/signer/signer.go index b38a815..7e74581 100644 --- a/signer/signer.go +++ b/signer/signer.go @@ -102,8 +102,12 @@ func CreateCanonRequest(req *http.Request, payload []byte, crTemplate CanonReque if date := req.Header.Get("date"); date == "" { req.Header.Set("date", now.Local().Format(time.RFC1123)) } + crPayload := payload + if crPayload == nil { + crPayload = []byte("") + } sha := sha256.New() - sha.Write(payload) + sha.Write(crPayload) req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sha.Sum(nil))) req.Header.Set("X-Amz-Date", amzDate) if crTemplate.Creds.SecurityToken != "" {