diff --git a/Dockerfile b/Dockerfile index 0ff4a2e..6585164 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,5 +8,10 @@ COPY ./app/main.go ./ RUN go build -tags lambda.norpc -o main main.go # Copy artifacts to a clean image FROM public.ecr.aws/lambda/provided:al2023 +# Install git and zip using dnf (Amazon Linux 2023) +RUN dnf update -y && \ + dnf install -y git zip && \ + dnf clean all COPY --from=build /app/main ${LAMBDA_TASK_ROOT} -ENTRYPOINT [ "main" ] \ No newline at end of file +WORKDIR ${LAMBDA_TASK_ROOT} +ENTRYPOINT [ "./main" ] \ No newline at end of file diff --git a/app/main.go b/app/main.go index b710825..2ddf860 100644 --- a/app/main.go +++ b/app/main.go @@ -2,6 +2,9 @@ package main import ( "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "log" @@ -24,16 +27,67 @@ type Config struct { AWSRegion string } +type Response struct { + StatusCode int `json:"statusCode"` + Headers map[string]string `json:"headers"` + Body string `json:"body"` +} + var commandRunner = exec.Command -func handleRequest(ctx context.Context, event json.RawMessage) error { +// verifySignature computes an HMAC using the provided secret and compares it to the incoming signature. +func verifySignature(secret, body, signatureHeader string) bool { + // Assuming the header is in the format "sha256=" + const prefix = "sha256=" + if len(signatureHeader) < len(prefix) || signatureHeader[:len(prefix)] != prefix { + return false + } + receivedSig := signatureHeader[len(prefix):] + + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(body)) + expectedSig := hex.EncodeToString(mac.Sum(nil)) + return hmac.Equal([]byte(receivedSig), []byte(expectedSig)) +} + +func handleRequest(ctx context.Context, event json.RawMessage) (Response, error) { + // For demonstration, assume the event JSON includes a "body" and "headers" map. + var req struct { + Body string `json:"body"` + Headers map[string]string `json:"headers"` + } + if err := json.Unmarshal(event, &req); err != nil { + log.Printf("Error unmarshalling event: %v", err) + return Response{StatusCode: 400, Headers: map[string]string{"Content-Type": "application/json"}, Body: "{\"message\":\"Bad Request\"}"}, err + } + + secret := os.Getenv("WEBHOOK_SECRET") + if secret == "" { + log.Println("WEBHOOK_SECRET is not set") + return Response{StatusCode: 500, Headers: map[string]string{"Content-Type": "application/json"}, Body: "{\"message\":\"Server configuration error\"}"}, fmt.Errorf("WEBHOOK_SECRET is not set") + } + + signature := req.Headers["X-Hub-Signature-256"] // adjust this header name as appropriate. + if signature == "" || !verifySignature(secret, req.Body, signature) { + log.Println("Signature verification failed") + return Response{StatusCode: 401, Headers: map[string]string{"Content-Type": "application/json"}, Body: "{\"message\":\"Unauthorized\"}"}, fmt.Errorf("signature verification failed") + } + // Call your existing process (for example, runDeploymentProcess) if err := runDeploymentProcess(ctx); err != nil { // Log the error; you may also report it via CloudWatch alarms log.Printf("Deployment process failed: %v", err) - return err + return Response{ + StatusCode: 500, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: fmt.Sprintf("{\"message\": \"Deployment process failed: %v\"}", err), + }, err } - return nil + return Response{ + StatusCode: 200, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: "{\"message\": \"Success\"}", + }, nil } func main() { @@ -44,14 +98,14 @@ func runDeploymentProcess(ctx context.Context) error { cfg, err := loadConfig() if err != nil { - log.Fatalf("Configuration error: %v", err) + log.Printf("Configuration error: %v", err) return err } // Create a unique temp directory for this run repoDir, err := os.MkdirTemp("", "repo-*") if err != nil { - log.Fatalf("Error creating temporary directory: %v", err) + log.Printf("Error creating temporary directory: %v", err) return err } defer os.RemoveAll(repoDir) @@ -59,26 +113,26 @@ func runDeploymentProcess(ctx context.Context) error { // 1. Clone the repository if err := cloneRepository(ctx, cfg.RepoURL, cfg.RepoBranch, repoDir); err != nil { - log.Fatalf("Failure in cloning: %v", err) + log.Printf("Failure in cloning: %v", err) return err } // 2. Create a ZIP archive of the repository if err := createZipArchive(ctx, repoDir, zipFilePath); err != nil { - log.Fatalf("Failure in creating ZIP archive: %v", err) + log.Printf("Failure in creating ZIP archive: %v", err) return err } // 3. Upload the ZIP file to S3 cfg_s3, err := config.LoadDefaultConfig(ctx, config.WithRegion(cfg.AWSRegion)) if err != nil { - log.Fatalf("Error loading configuration: %v", err) + log.Printf("Error loading configuration: %v", err) return err } s3Client := s3.NewFromConfig(cfg_s3) uploader := manager.NewUploader(s3Client) if err := uploadToS3WithUploader(ctx, zipFilePath, cfg.S3Bucket, cfg.S3Key, uploader); err != nil { - log.Fatalf("Failure in uploading to S3: %v", err) + log.Printf("Failure in uploading to S3: %v", err) return err } @@ -149,7 +203,7 @@ func uploadToS3WithUploader(ctx context.Context, zipPath, bucket, key string, up // Open the ZIP file f, err := os.Open(zipPath) if err != nil { - log.Fatalf("Error opening ZIP file: %v", err) + return fmt.Errorf("Error opening ZIP file: %v", err) } defer f.Close() diff --git a/app/main_test.go b/app/main_test.go index ca76fe6..f6fb770 100644 --- a/app/main_test.go +++ b/app/main_test.go @@ -2,6 +2,9 @@ package main import ( "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" "fmt" "os" "os/exec" @@ -12,6 +15,57 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" ) +func TestVerifySignature_Valid(t *testing.T) { + secret := "mysecret" + body := "{\"message\":\"example\"}" + + // Compute the expected signature for the valid scenario. + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(body)) + expectedSig := hex.EncodeToString(mac.Sum(nil)) + + // Prepare the signature header in the "sha256=" format. + signatureHeader := "sha256=" + expectedSig + + if !verifySignature(secret, body, signatureHeader) { + t.Errorf("Expected true for valid signature, got false") + } +} + +func TestVerifySignature_InvalidSignature(t *testing.T) { + secret := "mysecret" + body := "{\"message\":\"example\"}" + + // Use an intentionally incorrect signature. + signatureHeader := "sha256=invalidsignature" + + if verifySignature(secret, body, signatureHeader) { + t.Errorf("Expected false for an invalid signature, but got true") + } +} + +func TestVerifySignature_MissingPrefix(t *testing.T) { + secret := "mysecret" + body := "{\"message\":\"example\"}" + + // Provide a header that does not start with "sha256=" + signatureHeader := "invalidprefix" + + if verifySignature(secret, body, signatureHeader) { + t.Errorf("Expected false when header is missing the required prefix, got true") + } +} + +func TestVerifySignature_EmptyHeader(t *testing.T) { + secret := "mysecret" + body := "{\"message\":\"example\"}" + signatureHeader := "" + + if verifySignature(secret, body, signatureHeader) { + t.Errorf("Expected false when header is empty, got true") + } +} + func TestLoadConfig_Success(t *testing.T) { // Set up environment variables for the test. os.Setenv("REPO_URL", "https://example.com/repo.git") diff --git a/template-secret-key.yaml b/template-secret-key.yaml new file mode 100644 index 0000000..e860883 --- /dev/null +++ b/template-secret-key.yaml @@ -0,0 +1,26 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Parameters: + WebhookSecret: + Type: String + Description: The number as a secret key of a webhook + +Resources: + + SecretForWebhook: + Type: AWS::SecretsManager::Secret + Properties: + Description: The number as a secret key of a webhook + SecretString: !Sub + - '{"secretNumber": "${WebhookSecret}"}' + - WebhookSecret: !Ref WebhookSecret + Tags: + - Key: Project + Value: Git-server + +Outputs: + ArnSecretForWebhook: + Description: ARN of secret key of a webhook + Value: !Ref SecretForWebhook + Export: + Name: SecretForWebhook-ARN \ No newline at end of file diff --git a/template.yaml b/template.yaml new file mode 100644 index 0000000..a2bf608 --- /dev/null +++ b/template.yaml @@ -0,0 +1,88 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Parameters: + StageName: + Type: String + Default: Prod + Description: Name of the API stage. + +Resources: + + MyLambdaRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: LambdaS3PutObjectPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - s3:PutObject + Resource: arn:aws:s3:::naputo-blog-source/* + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + PackageType: Image + ImageUri: 692859919890.dkr.ecr.ap-northeast-1.amazonaws.com/blog-deployment:latest + Timeout: 30 + MemorySize: 256 + Environment: + Variables: + REPO_URL: "https://git.n-daisuke897.com/nakada0907/n-daisuke897-blog.git" + REPO_BRANCH: "main" + S3_BUCKET: "naputo-blog-source" + S3_KEY: "source.zip" + WEBHOOK_SECRET: + Fn::Sub: + - "{{resolve:secretsmanager:${SecretArn}:SecretString:secretNumber:AWSCURRENT}}" + - SecretArn: + Fn::ImportValue: SecretForWebhook-ARN + Role: !GetAtt MyLambdaRole.Arn + Events: + ForgejoWebhook: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /forgejo-webhook + Method: POST + + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: !Ref StageName + EndpointConfiguration: REGIONAL + DefinitionBody: + openapi: "3.0.1" + info: + title: "Forgejo Webhook API" + version: "1.0" + paths: + /forgejo-webhook: + post: + summary: "Trigger Lambda via Forgejo Webhook" + x-amazon-apigateway-integration: + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyLambdaFunction.Arn}/invocations + httpMethod: POST + type: aws_proxy + responses: + '200': + description: "Successful response" + '400': + description: "Bad Request - Incorrect request payload format" + '401': + description: "Unauthorized - Signature verification failed" + '500': + description: "Server error - Deployment process failed" \ No newline at end of file