Skip to content

Commit

Permalink
Merge pull request #3 from bluecatengineering/EDG-13557
Browse files Browse the repository at this point in the history
EDG-13557: GET method
  • Loading branch information
francisf2 authored Nov 24, 2023
2 parents b36d13a + 9dfea3b commit e2f215f
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 17 deletions.
43 changes: 32 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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"
Expand All @@ -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

Expand Down
23 changes: 22 additions & 1 deletion aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -39,13 +40,34 @@ 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)
log.Error(fmt.Sprintf("Reading body failed: %s", err.Error()))
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)
Expand All @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
22 changes: 18 additions & 4 deletions s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion signer/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down

0 comments on commit e2f215f

Please sign in to comment.