// Package s3 implements an HTTP provider for solving the HTTP-01 challenge using AWS S3. package s3 import ( "bytes" "context" "errors" "fmt" "strings" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/go-acme/lego/v4/challenge/http01" ) // HTTPProvider implements ChallengeProvider for `http-01` challenge. type HTTPProvider struct { bucket string client *s3.Client } // NewHTTPProvider returns a HTTPProvider instance with a configured s3 bucket and aws session. // Credentials must be passed in the environment variables. func NewHTTPProvider(bucket string) (*HTTPProvider, error) { if bucket == "" { return nil, errors.New("s3: bucket name missing") } ctx := context.Background() cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return nil, fmt.Errorf("s3: unable to create AWS config: %w", err) } client := s3.NewFromConfig(cfg) return &HTTPProvider{ bucket: bucket, client: client, }, nil } // Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given s3 bucket. func (s *HTTPProvider) Present(domain, token, keyAuth string) error { ctx := context.Background() params := &s3.PutObjectInput{ ACL: "public-read", Bucket: aws.String(s.bucket), Key: aws.String(strings.Trim(http01.ChallengePath(token), "/")), Body: bytes.NewReader([]byte(keyAuth)), } _, err := s.client.PutObject(ctx, params) if err != nil { return fmt.Errorf("s3: failed to upload token to s3: %w", err) } return nil } // CleanUp removes the file created for the challenge. func (s *HTTPProvider) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() params := &s3.DeleteObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(strings.Trim(http01.ChallengePath(token), "/")), } _, err := s.client.DeleteObject(ctx, params) if err != nil { return fmt.Errorf("s3: could not remove file in s3 bucket after HTTP challenge: %w", err) } return nil }