diff --git a/aws/signer/v4/middleware.go b/aws/signer/v4/middleware.go index 626715709ff..272baf37e6d 100644 --- a/aws/signer/v4/middleware.go +++ b/aws/signer/v4/middleware.go @@ -102,6 +102,13 @@ func AddComputePayloadSHA256Middleware(stack *middleware.Stack) error { return stack.Build.Add(&computePayloadSHA256{}, middleware.After) } +// RemoveComputePayloadSHA256Middleware removes computePayloadSHA256 from the +// operation middleware stack +func RemoveComputePayloadSHA256Middleware(stack *middleware.Stack) error { + _, err := stack.Build.Remove(computePayloadHashMiddlewareID) + return err +} + // ID is the middleware name func (m *computePayloadSHA256) ID() string { return computePayloadHashMiddlewareID @@ -159,6 +166,13 @@ func AddContentSHA256HeaderMiddleware(stack *middleware.Stack) error { return stack.Build.Insert(&contentSHA256Header{}, computePayloadHashMiddlewareID, middleware.After) } +// RemoveContentSHA256HeaderMiddleware removes contentSHA256Header middleware +// from the operation middleware stack +func RemoveContentSHA256HeaderMiddleware(stack *middleware.Stack) error { + _, err := stack.Build.Remove((*contentSHA256Header)(nil).ID()) + return err +} + // ID returns the ContentSHA256HeaderMiddleware identifier func (m *contentSHA256Header) ID() string { return "SigV4ContentSHA256Header" diff --git a/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AwsHttpPresignURLClientGenerator.java b/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AwsHttpPresignURLClientGenerator.java index 5dc35c03bd5..eb0981038a7 100644 --- a/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AwsHttpPresignURLClientGenerator.java +++ b/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AwsHttpPresignURLClientGenerator.java @@ -17,102 +17,231 @@ package software.amazon.smithy.aws.go.codegen; -import java.util.ArrayList; -import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; import software.amazon.smithy.aws.go.codegen.customization.AwsCustomGoDependency; +import software.amazon.smithy.aws.go.codegen.customization.PresignURLAutoFill; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.aws.traits.protocols.AwsQueryTrait; +import software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.go.codegen.GoDelegator; +import software.amazon.smithy.go.codegen.GoSettings; import software.amazon.smithy.go.codegen.GoWriter; import software.amazon.smithy.go.codegen.OperationGenerator; import software.amazon.smithy.go.codegen.SmithyGoDependency; import software.amazon.smithy.go.codegen.SymbolUtils; -import software.amazon.smithy.go.codegen.integration.ProtocolUtils; +import software.amazon.smithy.go.codegen.integration.GoIntegration; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.utils.SmithyBuilder; - -public class AwsHttpPresignURLClientGenerator { +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.StreamingTrait; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SetUtils; + +/** + * AwsHttpPresignURLClientGenerator class is a runtime plugin integration class + * that generates code for presign URL clients and associated presign operations. + *

+ * This class pulls in a static list from PresignURLAutofill customization which + * rely on the generated presigned url client and operation. This is done to + * deduplicate the listing but make this class dependent on presence of PresignURLAutofill + * class as a composition. + */ +public class AwsHttpPresignURLClientGenerator implements GoIntegration { + // constants private static final String CONVERT_TO_PRESIGN_MIDDLEWARE_NAME = "convertToPresignMiddleware"; - private final Model model; - private final SymbolProvider symbolProvider; + private static final String PRESIGN_CLIENT = "PresignClient"; + private static final Symbol presignClientSymbol = buildSymbol(PRESIGN_CLIENT, true); + + private static final String NEW_CLIENT = "NewPresignClient"; + private static final String PRESIGN_OPTIONS = "PresignOptions"; + private static final Symbol presignOptionsSymbol = buildSymbol(PRESIGN_OPTIONS, true); + + private static final String PRESIGN_OPTIONS_FROM_CLIENT_OPTIONS = "WithPresignClientFromClientOptions"; + private static final String PRESIGN_OPTIONS_FROM_EXPIRES = "WithPresignExpires"; + + private static final Symbol presignerInterfaceSymbol = SymbolUtils.createPointableSymbolBuilder( + "HTTPPresignerV4" + ).build(); + private static final Symbol v4NewPresignerSymbol = SymbolUtils.createPointableSymbolBuilder( + "NewSigner", AwsGoDependency.AWS_SIGNER_V4 + ).build(); + private static final Symbol v4PresignedHTTPRequestSymbol = SymbolUtils.createPointableSymbolBuilder( + "PresignedHTTPRequest", AwsGoDependency.AWS_SIGNER_V4 + ).build(); + + // constant map with service to list of operation for which presignedURL client and operation must be generated. + private static final Map> presignedClientMap = MapUtils.of( + ShapeId.from("com.amazonaws.s3#AmazonS3"), SetUtils.of( + ShapeId.from("com.amazonaws.s3#GetObject"), + ShapeId.from("com.amazonaws.s3#PutObject") + ) + ); + + private static final String addAsUnsignedPayloadName(String operationName) { + return String.format("add%sPayloadAsUnsigned", operationName); + } - private final Symbol presignClientSymbol; - private final Symbol newPresignClientSymbol; + // map of service to list of operations for which presignedURL client and operation should + // be generated. + public static Map> PRESIGNER_MAP = new TreeMap<>(); - private final OperationShape operation; - private final Symbol operationSymbol; - private final Shape operationInput; - private final Symbol operationInputSymbol; + // build pointable symbols + private static Symbol buildSymbol(String name, boolean exported) { + if (!exported) { + name = Character.toLowerCase(name.charAt(0)) + name.substring(1); + } + return SymbolUtils.createPointableSymbolBuilder(name).build(); + } - private final boolean exported; + /** + * generates code to iterate thru func optionals and assign value into the dest variable + * + * @param writer GoWriter to write the code to + * @param src variable name that denotes functional options + * @param dest variable in which result of processed functional options are stored + * @param destType value type used by functional options + */ + private static final void processFunctionalOptions( + GoWriter writer, + String src, + String dest, + Symbol destType + ) { + writer.write("var $L $T", dest, destType); + writer.openBlock("for _, fn := range $L {", "}", src, () -> { + writer.write("fn(&$L)", dest); + }).insertTrailingNewline(); + } - private final List convertToPresignMiddlewareHelpers; + @Override + public void processFinalizedModel(GoSettings settings, Model model) { + PRESIGNER_MAP.putAll(presignedClientMap); + + // update map for presign client/operation generation to include + // service/operations that use PresignURLAutoFill customization class. + Map> autofillMap = PresignURLAutoFill.SERVICE_TO_OPERATION_MAP; + for (ShapeId service : autofillMap.keySet()) { + if (!PRESIGNER_MAP.containsKey(service)) { + PRESIGNER_MAP.put(service, autofillMap.get(service)); + } else { + Set operations = new TreeSet<>(); + for (ShapeId operation : PRESIGNER_MAP.get(service)) { + operations.add(operation); + } + for (ShapeId operation : autofillMap.get(service)) { + operations.add(operation); + } + PRESIGNER_MAP.put(service, operations); + } + } + } - private AwsHttpPresignURLClientGenerator(Builder builder) { - this.exported = builder.exported; + @Override + public byte getOrder() { + // The associated customization ordering is relative to operation deserializers + // and thus the integration should be added at the end. + return 127; + } - this.model = SmithyBuilder.requiredState("model", builder.model); - this.symbolProvider = SmithyBuilder.requiredState("symbolProvider", builder.symbolProvider); - this.convertToPresignMiddlewareHelpers = builder.convertToPresignMiddlewareHelpers; + @Override + public void writeAdditionalFiles( + GoSettings settings, + Model model, + SymbolProvider symbolProvider, + GoDelegator goDelegator + ) { + ServiceShape serviceShape = settings.getService(model); + if (!PRESIGNER_MAP.containsKey(serviceShape.getId())) { + return; + } - this.operation = SmithyBuilder.requiredState("operation", builder.operation); - this.operationSymbol = symbolProvider.toSymbol(operation); + Set validOperations = PRESIGNER_MAP.get(serviceShape.getId()); + if (validOperations.isEmpty()) { + return; + } - this.operationInput = ProtocolUtils.expectInput(model, operation); - this.operationInputSymbol = symbolProvider.toSymbol(operationInput); + // delegator for service shape + goDelegator.useShapeWriter(serviceShape, (writer) -> { + // generate presigner interface + writePresignInterface(writer, model, symbolProvider, serviceShape); - this.presignClientSymbol = buildPresignClientSymbol(operationSymbol, exported); - this.newPresignClientSymbol = buildNewPresignClientSymbol(operationSymbol, exported); - } + // generate presign options and helpers per service + writePresignOptionType(writer, model, symbolProvider, serviceShape); - /** - * Writes the Presign client's type and methods. - * - * @param writer writer to write to - */ - public void writePresignClientType(GoWriter writer) { - writer.addUseImports(SmithyGoDependency.CONTEXT); - writer.addUseImports(AwsGoDependency.AWS_SIGNER_V4); + // generate Presign client per service + writePresignClientType(writer, model, symbolProvider, serviceShape); - writer.openBlock("type $T struct {", "}", presignClientSymbol, () -> { - writer.write("client *Client"); - writer.write("presigner *v4.Signer"); + // generate client helpers such as copyAPIClient, GetAPIClientOptions() + writePresignClientHelpers(writer, model, symbolProvider, serviceShape); + + // generate convertToPresignMiddleware per service + writeConvertToPresignMiddleware(writer, model, symbolProvider, serviceShape); }); - writer.openBlock("func $L(options Options, optFns ...func(*Options)) *$T {", "}", - newPresignClientSymbol.getName(), - presignClientSymbol, - () -> { - writer.openBlock("return &$T{", "}", presignClientSymbol, () -> { - writer.write("client: New(options, optFns...),"); + for (ShapeId operationId : serviceShape.getAllOperations()) { + OperationShape operationShape = model.expectShape(operationId, OperationShape.class); + if (!validOperations.contains(operationShape.getId())) { + continue; + } - writer.addUseImports(AwsGoDependency.AWS_SIGNER_V4); - writer.write("presigner: v4.NewSigner(),"); - }); - }); + goDelegator.useShapeWriter(operationShape, (writer) -> { + // generate presign operation function for a client operation. + writePresignOperationFunction(writer, model, symbolProvider, serviceShape, operationShape); - writer.addUseImports(SmithyGoDependency.NET_HTTP); - writer.openBlock( - // TODO presign with expire can be supported with a builder param that adds an additional expires param to presign signature. + // generate s3 unsigned payload middleware helper + writeS3AddAsUnsignedPayloadHelper(writer, model, symbolProvider, serviceShape, operationShape); + }); + } + } - // TODO Should this return a v4.PresignedHTTPRequest type instead of individual fields? - "func (c *$T) Presign$T(ctx context.Context, params $P, optFns ...func(*Options)) " - + "(string, http.Header, error) {", - "}", - presignClientSymbol, operationSymbol, operationInputSymbol, + private void writePresignOperationFunction( + GoWriter writer, + Model model, + SymbolProvider symbolProvider, + ServiceShape serviceShape, + OperationShape operationShape + ) { + Symbol operationSymbol = symbolProvider.toSymbol(operationShape); + + Shape operationInputShape = model.expectShape(operationShape.getInput().get()); + Symbol operationInputSymbol = symbolProvider.toSymbol(operationInputShape); + + writer.writeDocs( + String.format( + "Presign%s is used to generate a presigned HTTP Request which contains presigned URL, signed headers " + + "and HTTP method used.", operationSymbol.getName()) + ); + writer.openBlock( + "func (c *$T) Presign$T(ctx context.Context, params $P, optFns ...func($P)) " + + "($P, error) {", "}", presignClientSymbol, operationSymbol, + operationInputSymbol, presignOptionsSymbol, v4PresignedHTTPRequestSymbol, () -> { Symbol nopClient = SymbolUtils.createPointableSymbolBuilder("NopClient", SmithyGoDependency.SMITHY_HTTP_TRANSPORT) .build(); - writer.write("if params == nil { params = &$T{} }", operationInputSymbol); + writer.write("if params == nil { params = &$T{} }", operationInputSymbol).insertTrailingNewline(); + + // process presignerOptions + processFunctionalOptions(writer, "optFns", "presignOptions", presignOptionsSymbol); + + // check if presigner was set for presignerOptions + writer.openBlock("if len(optFns) != 0 {", "}", () -> { + writer.write("c = $L(c.client, optFns...)", NEW_CLIENT); + }); writer.write(""); - // TODO could be replaced with a `WithAPIOptions` client option helper. - // TODO could be replaced with a `WithHTTPClient` client option helper. - writer.openBlock("optFns = append(optFns, func(o *Options) {", "})", () -> { + writer.write("clientOptFns := make([]func(o *Options), 0)"); + writer.openBlock("clientOptFns = append(clientOptFns, func(o *Options) {", "})", () -> { writer.write("o.HTTPClient = &$T{}", nopClient); }); writer.write(""); @@ -121,19 +250,80 @@ public void writePresignClientType(GoWriter writer) { AwsCustomGoDependency.PRESIGNEDURL_CUSTOMIZATION).build(); writer.write("ctx = $T(ctx)", withIsPresigning); - writer.openBlock("result, _, err := c.client.invokeOperation(ctx, $S, params, optFns,", ")", + writer.openBlock("result, _, err := c.client.invokeOperation(ctx, $S, params, clientOptFns,", ")", operationSymbol.getName(), () -> { writer.write("$L,", OperationGenerator .getAddOperationMiddlewareFuncName(operationSymbol)); writer.write("c.$L,", CONVERT_TO_PRESIGN_MIDDLEWARE_NAME); + + // s3 should add a middleware where it switches to using unisgned payload if + // input is a stream. + if (isS3ServiceShape(model, serviceShape)) { + if (operationInputShape.members().stream().anyMatch(memberShape -> { + return memberShape.getMemberTrait(model, StreamingTrait.class).isPresent(); + })) { + writer.write("$L,", addAsUnsignedPayloadName(operationSymbol.getName())); + } + } }); - writer.write("if err != nil { return ``, nil, err }"); + writer.write("if err != nil { return nil, err }"); writer.write(""); - writer.write("out := result.(*v4.PresignedHTTPRequest)"); - writer.write("return out.URL, out.SignedHeader, nil"); + writer.write("out := result.($P)", v4PresignedHTTPRequestSymbol); + writer.write("return out, nil"); }); + writer.write(""); + } + + private void writeS3AddAsUnsignedPayloadHelper( + GoWriter writer, + Model model, + SymbolProvider symbolProvider, + ServiceShape serviceShape, + OperationShape operationShape + ) { + // if service is not s3, return + if (!isS3ServiceShape(model, serviceShape)) { + return; + } + + Symbol operationSymbol = symbolProvider.toSymbol(operationShape); + Shape operationInputShape = model.expectShape(operationShape.getInput().get()); + + // return if not streaming + if (operationInputShape.members().stream().noneMatch(memberShape -> { + return memberShape.getMemberTrait(model, StreamingTrait.class).isPresent(); + })) { return; } + + writer.openBlock("func $L(stack $P, options Options) error {", "}", + addAsUnsignedPayloadName(operationSymbol.getName()), + SymbolUtils.createPointableSymbolBuilder("Stack", SmithyGoDependency.SMITHY_MIDDLEWARE).build(), + () -> { + writer.addUseImports(AwsGoDependency.AWS_SIGNER_V4); + writer.write("v4.RemoveContentSHA256HeaderMiddleware(stack)"); + writer.write("v4.RemoveComputePayloadSHA256Middleware(stack)"); + + writer.write("return $T(stack)", SymbolUtils.createValueSymbolBuilder( + "AddUnsignedPayloadMiddleware", AwsGoDependency.AWS_SIGNER_V4).build()); + }); + writer.write(""); + } + + /** + * generates a helper to mutate request middleware stack in favor of generating a presign URL request + * + * @param writer the writer to write to + * @param model the service model + * @param symbolProvider the symbol provider + * @param serviceShape the service for which helper is generated + */ + private void writeConvertToPresignMiddleware( + GoWriter writer, + Model model, + SymbolProvider symbolProvider, + ServiceShape serviceShape + ) { Symbol smithyStack = SymbolUtils.createPointableSymbolBuilder("Stack", SmithyGoDependency.SMITHY_MIDDLEWARE) .build(); @@ -166,138 +356,215 @@ public void writePresignClientType(GoWriter writer) { presignMiddleware, smithyAfter); writer.write("if err != nil { return err }"); - convertToPresignMiddlewareHelpers.forEach((symbol) -> { - writer.write("err = $T(stack)", symbol); + // if protocol used is ec2query or query + if (serviceShape.hasTrait(AwsQueryTrait.ID) || serviceShape.hasTrait(Ec2QueryTrait.ID)) { + // presigned url should convert to Get request + Symbol queryAsGetMiddleware = SymbolUtils.createValueSymbolBuilder("AddAsGetRequestMiddleware", + AwsGoDependency.AWS_QUERY_PROTOCOL).build(); + + writer.writeDocs("convert request to a GET request"); + writer.write("err = $T(stack)", queryAsGetMiddleware); writer.write("if err != nil { return err }"); - }); + } + + // s3 service needs expires and sets unsignedPayload if input is stream + if (isS3ServiceShape(model, serviceShape)) { + Symbol expiresAsHeaderMiddleware = SymbolUtils.createValueSymbolBuilder( + "AddExpiresOnPresignedURL", + AwsCustomGoDependency.S3_CUSTOMIZATION).build(); + writer.writeDocs("add middleware to set expiration for s3 presigned url, " + + " if expiration is set to 0, this middleware sets a default expiration of 900 seconds"); + writer.write("err = stack.Build.Add(&$T{ Expires: c.expires, }, middleware.After)", + expiresAsHeaderMiddleware); + writer.write("if err != nil { return err }"); + } writer.write("return nil"); - }); + }).insertTrailingNewline(); } - public Symbol getPresignClientSymbol() { - return presignClientSymbol; - } - public Symbol getNewPresignClientSymbol() { - return newPresignClientSymbol; - } + /** + * Writes the Presign client's type and methods. + * + * @param writer writer to write to + */ + private void writePresignClientType( + GoWriter writer, + Model model, + SymbolProvider symbolProvider, + ServiceShape serviceShape + ) { + writer.addUseImports(SmithyGoDependency.CONTEXT); + writer.addUseImports(AwsGoDependency.AWS_SIGNER_V4); - private static Symbol buildNewPresignClientSymbol(Symbol operation, boolean exported) { - String name = String.format("New%sHTTPPresignURLClient", operation.getName()); - return buildSymbol(name, exported); - } + writer.writeDocs(String.format("%s represents the presign url client", PRESIGN_CLIENT)); + writer.openBlock("type $T struct {", "}", presignClientSymbol, () -> { + writer.write("client *Client"); + writer.write("presigner $T", presignerInterfaceSymbol); - private static Symbol buildPresignClientSymbol(Symbol operation, boolean exported) { - String name = String.format("%sHTTPPresignURLClient", operation.getName()); - return buildSymbol(name, exported); - } + if (isS3ServiceShape(model, serviceShape)) { + writer.addUseImports(SmithyGoDependency.TIME); + writer.write("expires time.Duration"); + } + }); + writer.write(""); + + // generate NewPresignClient + writer.writeDocs( + String.format("%s generates a presign client using provided API Client and presign options", + NEW_CLIENT) + ); + writer.openBlock("func $L(c *Client, optFns ...func($P)) $P {", "}", + NEW_CLIENT, presignOptionsSymbol, presignClientSymbol, () -> { + processFunctionalOptions(writer, "optFns", "presignOptions", presignOptionsSymbol); + writer.write("client := copyAPIClient(c, presignOptions.ClientOptions...)"); + writer.openBlock("if presignOptions.Presigner == nil {", "}", () -> { + writer.write("presignOptions.Presigner = $T()", v4NewPresignerSymbol); + }); - private static Symbol buildAPIClientSymbol(Symbol operation, boolean exported) { - String name = String.format("%sAPIClient", operation.getName()); - return buildSymbol(name, exported); + writer.write(""); + writer.openBlock("return &$L{", "}", presignClientSymbol, () -> { + writer.write("client: client,"); + writer.write("presigner: presignOptions.Presigner,"); + // if s3 assign expires value on client + if (isS3ServiceShape(model, serviceShape)) { + writer.write("expires: presignOptions.Expires,"); + } + }); + }); + writer.write(""); } - private static Symbol buildSymbol(String name, boolean exported) { - if (!exported) { - name = Character.toLowerCase(name.charAt(0)) + name.substring(1); - } - return SymbolUtils.createValueSymbolBuilder(name). - build(); + /** + * Writes the Presign client's helper methods. + * + * @param writer writer to write to + */ + private void writePresignClientHelpers( + GoWriter writer, + Model model, + SymbolProvider symbolProvider, + ServiceShape serviceShape + ) { + // generate copy API client + final String COPY_API_CLIENT = "copyAPIClient"; + writer.openBlock("func $L(c *Client, optFns ...func(*Options)) *Client {", "}", + COPY_API_CLIENT, () -> { + writer.write("return New(c.options, optFns...)"); + writer.insertTrailingNewline(); + }); + writer.write(""); } /** - * Builder for the HTTP Presign URL client client generator. + * Writes the presigner interface used by the presign url client */ - public static class Builder implements SmithyBuilder { - private Model model; - private SymbolProvider symbolProvider; - private OperationShape operation; - private boolean exported; - private List convertToPresignMiddlewareHelpers = new ArrayList<>(); - - /** - * Sets the model for the builder - * - * @param model API model - * @return builder - */ - public Builder model(Model model) { - this.model = model; - return this; - } + public void writePresignInterface( + GoWriter writer, + Model model, + SymbolProvider symbolProvider, + ServiceShape serviceShape + ) { + writer.writeDocs( + String.format("%s represents presigner interface used by presign url client", + presignerInterfaceSymbol.getName()) + ); + writer.openBlock("type $T interface {", "}", presignerInterfaceSymbol, () -> { + writer.write("PresignHTTP("); + writer.write("ctx context.Context, credentials aws.Credentials, r *http.Request, "); + writer.write("payloadHash string, service string, region string, signingTime time.Time, "); + writer.write(") (url string, signedHeader http.Header, err error)"); + }); - /** - * Sets the symbol provider for the builder - * - * @param symbolProvider the symbol provider - * @return buidler - */ - public Builder symbolProvider(SymbolProvider symbolProvider) { - this.symbolProvider = symbolProvider; - return this; - } + writer.write(""); + } - /** - * Sets the operation for the builder - * - * @param operation api operation - * @return builder - */ - public Builder operation(OperationShape operation) { - this.operation = operation; - return this; - } + /** + * Writes the Presign client's type and methods. + * + * @param writer writer to write to + */ + public void writePresignOptionType( + GoWriter writer, + Model model, + SymbolProvider symbolProvider, + ServiceShape serviceShape + ) { + writer.addUseImports(SmithyGoDependency.CONTEXT); + Symbol presignOptionSymbol = buildSymbol(PRESIGN_OPTIONS, true); + + // generate presign options + writer.writeDocs(String.format("%s represents the presign client options", PRESIGN_OPTIONS)); + writer.openBlock("type $T struct {", "}", presignOptionSymbol, () -> { + writer.write(""); + writer.writeDocs( + "ClientOptions are list of functional options to mutate client options used by presign client" + ); + writer.write("ClientOptions []func(*Options)"); + + writer.write(""); + writer.writeDocs("Presigner is the presigner used by the presign url client"); + writer.write("Presigner $T", presignerInterfaceSymbol); + + // s3 service has an additional Expires options + if (isS3ServiceShape(model, serviceShape)) { + writer.write(""); + writer.writeDocs( + String.format("Expires sets the expiration duration for the generated presign url. This should " + + "be the duration in seconds the presigned URL should be considered valid for. If " + + "not set or set to zero, presign url would default to expire after 900 seconds." + ) + ); + writer.write("Expires time.Duration"); + } + }); - /** - * Sets that the generated client type should be exported, defaults to false. - * - * @return builder - */ - public Builder exported() { - return this.exported(true); - } + // generate WithPresignClientFromClientOptions Helper + Symbol presignOptionsFromClientOptionsInternal = buildSymbol(PRESIGN_OPTIONS_FROM_CLIENT_OPTIONS, false); + writer.writeDocs( + String.format("%s is a helper utility to retrieve a function that takes PresignOption as input", + PRESIGN_OPTIONS_FROM_CLIENT_OPTIONS) + ); + writer.openBlock("func $L(optFns ...func(*Options)) func($P) {", "}", + PRESIGN_OPTIONS_FROM_CLIENT_OPTIONS, presignOptionSymbol, () -> { + writer.write("return $L(optFns).options", presignOptionsFromClientOptionsInternal.getName()); + }); - /** - * Sets if the generate client type should be exported or not. - * - * @param exported if exported - * @return builder - */ - public Builder exported(boolean exported) { - this.exported = exported; - return this; - } + writer.insertTrailingNewline(); + + writer.write("type $L []func(*Options)", presignOptionsFromClientOptionsInternal.getName()); + writer.openBlock("func (w $L) options (o $P) {", "}", + presignOptionsFromClientOptionsInternal.getName(), presignOptionSymbol, () -> { + writer.write("o.ClientOptions = append(o.ClientOptions, w...)"); + }).insertTrailingNewline(); + + + // s3 specific helpers + if (isS3ServiceShape(model, serviceShape)) { + // generate WithPresignExpires Helper + Symbol presignOptionsFromExpiresInternal = buildSymbol(PRESIGN_OPTIONS_FROM_EXPIRES, false); + writer.writeDocs(String.format( + "%s is a helper utility to append Expires value on presign options optional function", + PRESIGN_OPTIONS_FROM_EXPIRES)); + writer.openBlock("func $L(dur time.Duration) func($P) {", "}", + PRESIGN_OPTIONS_FROM_EXPIRES, presignOptionSymbol, () -> { + writer.write("return $L(dur).options", presignOptionsFromExpiresInternal.getName()); + }); - /** - * Sets additional middleware mutator that will be generated into the client's convert to presign URL operation. - * Used by the client to convert a API operation to a presign URL. - * - * @param middlewareHelpers list of middleware helpers to set - * @return builder - */ - public Builder convertToPresignMiddlewareHelpers(List middlewareHelpers) { - this.convertToPresignMiddlewareHelpers.clear(); - this.convertToPresignMiddlewareHelpers.addAll(middlewareHelpers); - return this; - } + writer.insertTrailingNewline(); - /** - * Adds a single middleware mutator that will be generated into the client's convert to presign URL operation. - * Used by the client to convert API operation to a presigned URL. - * - * @param middlewareHelper the middleware helper to add - * @return builder - */ - public Builder addConvertToPresignMiddlewareHelpers(Symbol middlewareHelper) { - this.convertToPresignMiddlewareHelpers.add(middlewareHelper); - return this; + writer.write("type $L time.Duration", presignOptionsFromExpiresInternal.getName()); + writer.openBlock("func (w $L) options (o $P) {", "}", + presignOptionsFromExpiresInternal.getName(), presignOptionSymbol, () -> { + writer.write("o.Expires = time.Duration(w)"); + }).insertTrailingNewline(); } + } - // TODO presign with expire can be supported with a builder param that enables expires param behavior. - - public AwsHttpPresignURLClientGenerator build() { - return new AwsHttpPresignURLClientGenerator(this); - } + private final boolean isS3ServiceShape(Model model, ServiceShape service) { + String serviceId = service.expectTrait(ServiceTrait.class).getSdkId(); + return serviceId.equalsIgnoreCase("S3"); } } + diff --git a/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/customization/PresignURLAutoFill.java b/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/customization/PresignURLAutoFill.java index ef93b8c3de5..6a606746556 100644 --- a/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/customization/PresignURLAutoFill.java +++ b/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/customization/PresignURLAutoFill.java @@ -24,8 +24,6 @@ import java.util.TreeMap; import java.util.logging.Logger; import software.amazon.smithy.aws.go.codegen.AwsGoDependency; -import software.amazon.smithy.aws.go.codegen.AwsHttpPresignURLClientGenerator; -import software.amazon.smithy.codegen.core.CodegenException; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.go.codegen.CodegenUtils; @@ -54,12 +52,17 @@ import software.amazon.smithy.utils.MapUtils; import software.amazon.smithy.utils.SetUtils; +/** + * PresignURLAutoFill represents a runtime plugin integration class + * used generate customization to autofill a presign url as + * an unexported serialized input member + */ public class PresignURLAutoFill implements GoIntegration { - private static final Logger LOGGER = Logger.getLogger(PresignURLAutoFill.class.getName()); - - private final List runtimeClientPlugins = new ArrayList<>(); - - private static final Map> SERVICE_TO_OPERATION_MAP = MapUtils.of( + /** + * Map of service shape to Set of operation shapes that need to have this + * presigned url auto fill customization. + */ + public static final Map> SERVICE_TO_OPERATION_MAP = MapUtils.of( ShapeId.from("com.amazonaws.rds#AmazonRDSv19"), SetUtils.of( ShapeId.from("com.amazonaws.rds#CopyDBSnapshot"), ShapeId.from("com.amazonaws.rds#CreateDBInstanceReadReplica"), @@ -71,6 +74,82 @@ public class PresignURLAutoFill implements GoIntegration { // TODO other services ); + private static final Logger LOGGER = Logger.getLogger(PresignURLAutoFill.class.getName()); + private final List runtimeClientPlugins = new ArrayList<>(); + + private static void writeMemberSetter( + GoWriter writer, + SymbolProvider symbolprovider, + OperationShape operation, + StructureShape input, + MemberShape member + ) { + Symbol operationSymbol = symbolprovider.toSymbol(operation); + Symbol inputSymbol = symbolprovider.toSymbol(input); + String memberName = symbolprovider.toMemberName(member); + + writer.openBlock("func $L(params interface{}, value string) error {", "}", + setterFuncName(operationSymbol.getName(), memberName), + () -> { + writer.addUseImports(SmithyGoDependency.FMT); + writer.write("input, ok := params.($P)", inputSymbol); + writer.openBlock("if !ok {", "}", () -> { + writer.write("return fmt.Errorf(\"expect $P type, got %T\", params)", inputSymbol); + }); + writer.write("input.$L = &value", memberName); + writer.write("return nil"); + }); + } + + private static void writeMemberGetter( + GoWriter writer, + SymbolProvider symbolprovider, + OperationShape operation, + StructureShape input, + MemberShape member + ) { + Symbol operationSymbol = symbolprovider.toSymbol(operation); + Symbol inputSymbol = symbolprovider.toSymbol(input); + String memberName = symbolprovider.toMemberName(member); + + writer.openBlock("func $L(params interface{}) (string, bool, error) {", "}", + getterFuncName(operationSymbol.getName(), memberName), + () -> { + writer.addUseImports(SmithyGoDependency.FMT); + writer.write("input, ok := params.($P)", inputSymbol); + writer.openBlock("if !ok {", "}", () -> { + writer.write("return ``, false, fmt.Errorf(\"expect $P type, got %T\", params)", inputSymbol); + }); + writer.openBlock("if input.$L == nil || len(*input.$L) == 0 {", "}", memberName, memberName, + () -> writer.write("return ``, false, nil") + ); + writer.write("return *input.$L, true, nil", memberName); + }); + } + + private static String addPresignMiddlewareFuncName(String operationName) { + return String.format("add%sPresignURLMiddleware", operationName); + } + + private static String getterFuncName(String operationName, String memberName) { + return String.format("get%s%s", operationName, memberName); + } + + private static String setterFuncName(String operationName, String memberName) { + return String.format("set%s%s", operationName, memberName); + } + + private static String copyInputFuncName(String inputName) { + return String.format("copy%sForPresign", inputName); + } + + private static String presignFuncName(String operationName) { + return String.format("Presign%s", operationName); + } + + private static String autofillPresignClient(String operationName) { + return String.format("presignAutoFill%sClient", operationName); + } /** * Updates the API model to add additional members to the operation input shape that are needed for presign url @@ -164,6 +243,9 @@ public void writeAdditionalFiles( // Generate the presign client writePresignClientCustomization(writer, settings, model, symbolProvider, operation, input); + + // Generate the autofill presign client and its PresignURL method + writeAutofillPresignClient(writer, symbolProvider, operation, input); }); goDelegator.useShapeTestWriter(operation, (writer) -> { @@ -250,56 +332,6 @@ private void writeMemberAccessor( } } - private static void writeMemberSetter( - GoWriter writer, - SymbolProvider symbolprovider, - OperationShape operation, - StructureShape input, - MemberShape member - ) { - Symbol operationSymbol = symbolprovider.toSymbol(operation); - Symbol inputSymbol = symbolprovider.toSymbol(input); - String memberName = symbolprovider.toMemberName(member); - - writer.openBlock("func $L(params interface{}, value string) error {", "}", - setterFuncName(operationSymbol.getName(), memberName), - () -> { - writer.addUseImports(SmithyGoDependency.FMT); - writer.write("input, ok := params.($P)", inputSymbol); - writer.openBlock("if !ok {", "}", () -> { - writer.write("return fmt.Errorf(\"expect $P type, got %T\", params)", inputSymbol); - }); - writer.write("input.$L = &value", memberName); - writer.write("return nil"); - }); - } - - private static void writeMemberGetter( - GoWriter writer, - SymbolProvider symbolprovider, - OperationShape operation, - StructureShape input, - MemberShape member - ) { - Symbol operationSymbol = symbolprovider.toSymbol(operation); - Symbol inputSymbol = symbolprovider.toSymbol(input); - String memberName = symbolprovider.toMemberName(member); - - writer.openBlock("func $L(params interface{}) (string, bool, error) {", "}", - getterFuncName(operationSymbol.getName(), memberName), - () -> { - writer.addUseImports(SmithyGoDependency.FMT); - writer.write("input, ok := params.($P)", inputSymbol); - writer.openBlock("if !ok {", "}", () -> { - writer.write("return ``, false, fmt.Errorf(\"expect $P type, got %T\", params)", inputSymbol); - }); - writer.openBlock("if input.$L == nil || len(*input.$L) == 0 {", "}", memberName, memberName, - () -> writer.write("return ``, false, nil") - ); - writer.write("return *input.$L, true, nil", memberName); - }); - } - private void writePresignClientCustomization( GoWriter writer, GoSettings settings, @@ -308,29 +340,6 @@ private void writePresignClientCustomization( OperationShape operation, StructureShape input ) { - AwsHttpPresignURLClientGenerator.Builder clientGenBuilder = new AwsHttpPresignURLClientGenerator.Builder() - .model(model) - .symbolProvider(symbolProvider) - .operation(operation); - - switch (settings.getProtocol().toString()) { - case "aws.protocols#awsQuery": - case "aws.protocols#ec2Query": - Symbol queryAsGetMiddleware = SymbolUtils.createValueSymbolBuilder("AddAsGetRequestMiddleware", - AwsGoDependency.AWS_QUERY_PROTOCOL) - .build(); - clientGenBuilder.addConvertToPresignMiddlewareHelpers(queryAsGetMiddleware); - break; - - default: - LOGGER.warning("presign url customization does not know how to handle service " - + settings.getService() + " using protocol "+ settings.getProtocol()); - } - - AwsHttpPresignURLClientGenerator gen = clientGenBuilder.build(); - - gen.writePresignClientType(writer); - Symbol smithyStack = SymbolUtils.createPointableSymbolBuilder("Stack", SmithyGoDependency.SMITHY_MIDDLEWARE).build(); Symbol operationSymbol = symbolProvider.toSymbol(operation); @@ -347,8 +356,7 @@ private void writePresignClientCustomization( AwsCustomGoDependency.PRESIGNEDURL_CUSTOMIZATION).build(); Symbol addMiddleware = SymbolUtils.createValueSymbolBuilder("AddMiddleware", AwsCustomGoDependency.PRESIGNEDURL_CUSTOMIZATION).build(); - Symbol removeMiddleware = SymbolUtils.createValueSymbolBuilder("RemoveMiddleware", - AwsCustomGoDependency.PRESIGNEDURL_CUSTOMIZATION).build(); + // generate middleware mutator to wire up presign client with accessors and custom middleware. writer.openBlock("func $L(stack $P, options Options) error {", "}", @@ -357,49 +365,69 @@ private void writePresignClientCustomization( () -> { writer.openBlock("return $T(stack, $T{", "})", addMiddleware, addMiddlewareOptions, () -> { writer.openBlock("Accessor: $T{", "},", parameterAccessor, () -> { - writer.write("GetPresignedURL: $L,", + writer.write("GetPresignedURL: $L, \n", getterFuncName(operationSymbol.getName(), presignURLMember)); - writer.write("GetSourceRegion: $L,", + writer.write("GetSourceRegion: $L, \n", getterFuncName(operationSymbol.getName(), srcRegionMember)); - writer.write("CopyInput: $L,", copyInputFuncName(inputSymbol.getName())); - writer.write("SetDestinationRegion: $L,", + writer.write("CopyInput: $L, \n", copyInputFuncName(inputSymbol.getName())); + writer.write("SetDestinationRegion: $L,\n", setterFuncName(operationSymbol.getName(), dstRegionMember)); - writer.write("SetPresignedURL: $L,", + writer.write("SetPresignedURL: $L, \n", setterFuncName(operationSymbol.getName(), presignURLMember)); }); - // Replace with type wrapping presigner for generic signature - writer.write("Presigner: &$L{client: $T(options)},", - opPresignClientWrapperName(operationSymbol.getName()), - gen.getNewPresignClientSymbol()); + writer.write("Presigner: &$L{ client: NewPresignClient(New(options))}, \n", + autofillPresignClient(operationSymbol.getName())); }); }); + } + + + private void writeAutofillPresignClient( + GoWriter writer, + SymbolProvider symbolprovider, + OperationShape operation, + StructureShape input + ) { + Symbol operationSymbol = symbolprovider.toSymbol(operation); + Symbol inputSymbol = symbolprovider.toSymbol(input); + Symbol removeMiddleware = SymbolUtils.createValueSymbolBuilder("RemoveMiddleware", + AwsCustomGoDependency.PRESIGNEDURL_CUSTOMIZATION).build(); - // Generate generic presign wrapper type for passing region in with op call. + // generate autofill presign client writer.openBlock("type $L struct {", "}", - opPresignClientWrapperName(operationSymbol.getName()), - () -> { - writer.write("client *$T", gen.getPresignClientSymbol()); + autofillPresignClient(operationSymbol.getName()), () -> { + writer.write("client *PresignClient"); }); - writer.addUseImports(SmithyGoDependency.NET_HTTP); - writer.openBlock( - // TODO consider creating type for presign parameters for future compatibility. - "func (c *$L) PresignURL(ctx context.Context, region string, params interface{}) " - + "(string, http.Header, error) {", "}", - opPresignClientWrapperName(operationSymbol.getName()), - () -> { + writer.write(""); + + // generate PresignURL method that satisfies URLPresigner interface of middleware + writer.writeDocs("PresignURL is a middleware accessor that satisfies URLPresigner interface."); + writer.openBlock("func (c *$L) PresignURL(ctx context.Context, srcRegion string, params interface{}) " + + "(*v4.PresignedHTTPRequest, error) {", "}", + + autofillPresignClient(operationSymbol.getName()), () -> { + writer.addUseImports(SmithyGoDependency.FMT); + // check input writer.write("input, ok := params.($P)", inputSymbol); writer.openBlock("if !ok {", "}", () -> { - writer.write("return ``, nil, fmt.Errorf(\"expect $P type, got %T\", params)", inputSymbol); + writer.write("return nil, fmt.Errorf(\"expect $P type, got %T\", params)", inputSymbol); }); - // TODO could be replaced with a `WithRegion` client option helper. + // generate client options writer.openBlock("optFn := func(o *Options) {", "}", () -> { - writer.write("o.Region = region"); + writer.write("o.Region = srcRegion"); writer.write("o.APIOptions = append(o.APIOptions, $T)", removeMiddleware); }); - writer.write("return c.client.Presign$L(ctx, input, optFn)", operationSymbol.getName()); + + // getPresignAPIOptions + writer.write("presignOptFn := WithPresignClientFromClientOptions(optFn)"); + + // call the exported function + writer.write("return c.client.$L(ctx, input, presignOptFn)", + presignFuncName(operationSymbol.getName())); }); + writer.write(""); } private void writePresignClientCustomizationTest( @@ -440,23 +468,4 @@ private void writePresignClientCustomizationTest( writer.write(template); } - private static String opPresignClientWrapperName(String operationName) { - return String.format("presignAutoFill%sClient", operationName); - } - - private static String addPresignMiddlewareFuncName(String operationName) { - return String.format("add%sPresignURLMiddleware", operationName); - } - - private static String getterFuncName(String operationName, String memberName) { - return String.format("get%s%s", operationName, memberName); - } - - private static String setterFuncName(String operationName, String memberName) { - return String.format("set%s%s", operationName, memberName); - } - - private static String copyInputFuncName(String inputName) { - return String.format("copy%sForPresign", inputName); - } } diff --git a/codegen/smithy-aws-go-codegen/src/main/resources/META-INF/services/software.amazon.smithy.go.codegen.integration.GoIntegration b/codegen/smithy-aws-go-codegen/src/main/resources/META-INF/services/software.amazon.smithy.go.codegen.integration.GoIntegration index a45b33d3479..7e1dfee480e 100644 --- a/codegen/smithy-aws-go-codegen/src/main/resources/META-INF/services/software.amazon.smithy.go.codegen.integration.GoIntegration +++ b/codegen/smithy-aws-go-codegen/src/main/resources/META-INF/services/software.amazon.smithy.go.codegen.integration.GoIntegration @@ -29,3 +29,4 @@ software.amazon.smithy.aws.go.codegen.customization.PresignURLAutoFill software.amazon.smithy.aws.go.codegen.FilterStreamingOperations software.amazon.smithy.aws.go.codegen.RequestResponseLogging software.amazon.smithy.aws.go.codegen.customization.S3ControlEndpointResolver +software.amazon.smithy.aws.go.codegen.AwsHttpPresignURLClientGenerator diff --git a/example/service/s3/listObjects/go.mod b/example/service/s3/listObjects/go.mod index 3bad0eece6d..e9f2a4f3273 100644 --- a/example/service/s3/listObjects/go.mod +++ b/example/service/s3/listObjects/go.mod @@ -23,3 +23,5 @@ replace github.com/aws/aws-sdk-go-v2/service/sts => ../../../../service/sts/ replace github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding => ../../../../service/internal/accept-encoding/ replace github.com/aws/aws-sdk-go-v2/service/internal/s3shared => ../../../../service/internal/s3shared/ + +replace github.com/aws/aws-sdk-go-v2/service/internal/presigned-url => ../../../../service/internal/presigned-url/ diff --git a/example/service/s3/listObjects/go.sum b/example/service/s3/listObjects/go.sum index 57ffc83dc1c..e1dc3ec43bf 100644 --- a/example/service/s3/listObjects/go.sum +++ b/example/service/s3/listObjects/go.sum @@ -5,6 +5,8 @@ github.com/awslabs/smithy-go v0.3.1-0.20201108010311-62c2a93810b4/go.mod h1:hPOQ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/feature/s3/manager/go.mod b/feature/s3/manager/go.mod index 45c728cce68..db589b6bbca 100644 --- a/feature/s3/manager/go.mod +++ b/feature/s3/manager/go.mod @@ -8,7 +8,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/s3shared v0.3.1 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v0.29.0 github.com/awslabs/smithy-go v0.3.1-0.20201108010311-62c2a93810b4 - github.com/google/go-cmp v0.4.1 + github.com/google/go-cmp v0.5.2 ) replace github.com/aws/aws-sdk-go-v2 => ../../../ @@ -26,3 +26,5 @@ replace github.com/aws/aws-sdk-go-v2/service/sts => ../../../service/sts/ replace github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding => ../../../service/internal/accept-encoding/ replace github.com/aws/aws-sdk-go-v2/service/internal/s3shared => ../../../service/internal/s3shared/ + +replace github.com/aws/aws-sdk-go-v2/service/internal/presigned-url => ../../../service/internal/presigned-url/ diff --git a/feature/s3/manager/go.sum b/feature/s3/manager/go.sum index 2f07ad94cff..75ff214e0e0 100644 --- a/feature/s3/manager/go.sum +++ b/feature/s3/manager/go.sum @@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= diff --git a/service/ec2/api_client.go b/service/ec2/api_client.go index 5b0ed68bc6a..3b4c1d0b7be 100644 --- a/service/ec2/api_client.go +++ b/service/ec2/api_client.go @@ -7,6 +7,7 @@ import ( cryptorand "crypto/rand" "github.com/aws/aws-sdk-go-v2/aws" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + "github.com/aws/aws-sdk-go-v2/aws/protocol/query" "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/aws/signer/v4" awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" @@ -246,3 +247,78 @@ func addRequestResponseLogging(stack *middleware.Stack, o Options) error { LogResponseWithBody: o.ClientLogMode.IsResponseWithBody(), }, middleware.After) } + +// HTTPPresignerV4 represents presigner interface used by presign url client +type HTTPPresignerV4 interface { + PresignHTTP( + ctx context.Context, credentials aws.Credentials, r *http.Request, + payloadHash string, service string, region string, signingTime time.Time, + ) (url string, signedHeader http.Header, err error) +} + +// PresignOptions represents the presign client options +type PresignOptions struct { + + // ClientOptions are list of functional options to mutate client options used by + // presign client + ClientOptions []func(*Options) + + // Presigner is the presigner used by the presign url client + Presigner HTTPPresignerV4 +} + +// WithPresignClientFromClientOptions is a helper utility to retrieve a function +// that takes PresignOption as input +func WithPresignClientFromClientOptions(optFns ...func(*Options)) func(*PresignOptions) { + return withPresignClientFromClientOptions(optFns).options +} + +type withPresignClientFromClientOptions []func(*Options) + +func (w withPresignClientFromClientOptions) options(o *PresignOptions) { + o.ClientOptions = append(o.ClientOptions, w...) +} + +// PresignClient represents the presign url client +type PresignClient struct { + client *Client + presigner HTTPPresignerV4 +} + +// NewPresignClient generates a presign client using provided API Client and +// presign options +func NewPresignClient(c *Client, optFns ...func(*PresignOptions)) *PresignClient { + var presignOptions PresignOptions + for _, fn := range optFns { + fn(&presignOptions) + } + client := copyAPIClient(c, presignOptions.ClientOptions...) + if presignOptions.Presigner == nil { + presignOptions.Presigner = v4.NewSigner() + } + + return &PresignClient{ + client: client, + presigner: presignOptions.Presigner, + } +} + +func copyAPIClient(c *Client, optFns ...func(*Options)) *Client { + return New(c.options, optFns...) +} + +func (c *PresignClient) convertToPresignMiddleware(stack *middleware.Stack, options Options) (err error) { + stack.Finalize.Clear() + stack.Deserialize.Clear() + stack.Build.Remove((*awsmiddleware.ClientRequestID)(nil).ID()) + err = stack.Finalize.Add(v4.NewPresignHTTPRequestMiddleware(options.Credentials, c.presigner), middleware.After) + if err != nil { + return err + } + // convert request to a GET request + err = query.AddAsGetRequestMiddleware(stack) + if err != nil { + return err + } + return nil +} diff --git a/service/ec2/api_op_CopySnapshot.go b/service/ec2/api_op_CopySnapshot.go index 9426ea2a80a..dec72096519 100644 --- a/service/ec2/api_op_CopySnapshot.go +++ b/service/ec2/api_op_CopySnapshot.go @@ -6,13 +6,11 @@ import ( "context" "fmt" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" - "github.com/aws/aws-sdk-go-v2/aws/protocol/query" "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "github.com/aws/aws-sdk-go-v2/service/ec2/types" presignedurlcust "github.com/aws/aws-sdk-go-v2/service/internal/presigned-url" "github.com/awslabs/smithy-go/middleware" smithyhttp "github.com/awslabs/smithy-go/transport/http" - "net/http" ) // Copies a point-in-time snapshot of an EBS volume and stores it in Amazon S3. You @@ -240,80 +238,39 @@ func setCopySnapshotdestinationRegion(params interface{}, value string) error { input.destinationRegion = &value return nil } - -type copySnapshotHTTPPresignURLClient struct { - client *Client - presigner *v4.Signer -} - -func newCopySnapshotHTTPPresignURLClient(options Options, optFns ...func(*Options)) *copySnapshotHTTPPresignURLClient { - return ©SnapshotHTTPPresignURLClient{ - client: New(options, optFns...), - presigner: v4.NewSigner(), - } -} -func (c *copySnapshotHTTPPresignURLClient) PresignCopySnapshot(ctx context.Context, params *CopySnapshotInput, optFns ...func(*Options)) (string, http.Header, error) { - if params == nil { - params = &CopySnapshotInput{} - } - - optFns = append(optFns, func(o *Options) { - o.HTTPClient = &smithyhttp.NopClient{} - }) - - ctx = presignedurlcust.WithIsPresigning(ctx) - result, _, err := c.client.invokeOperation(ctx, "CopySnapshot", params, optFns, - addOperationCopySnapshotMiddlewares, - c.convertToPresignMiddleware, - ) - if err != nil { - return ``, nil, err - } - - out := result.(*v4.PresignedHTTPRequest) - return out.URL, out.SignedHeader, nil -} -func (c *copySnapshotHTTPPresignURLClient) convertToPresignMiddleware(stack *middleware.Stack, options Options) (err error) { - stack.Finalize.Clear() - stack.Deserialize.Clear() - stack.Build.Remove((*awsmiddleware.ClientRequestID)(nil).ID()) - err = stack.Finalize.Add(v4.NewPresignHTTPRequestMiddleware(options.Credentials, c.presigner), middleware.After) - if err != nil { - return err - } - err = query.AddAsGetRequestMiddleware(stack) - if err != nil { - return err - } - return nil -} func addCopySnapshotPresignURLMiddleware(stack *middleware.Stack, options Options) error { return presignedurlcust.AddMiddleware(stack, presignedurlcust.Options{ Accessor: presignedurlcust.ParameterAccessor{ - GetPresignedURL: getCopySnapshotPresignedUrl, - GetSourceRegion: getCopySnapshotSourceRegion, - CopyInput: copyCopySnapshotInputForPresign, + GetPresignedURL: getCopySnapshotPresignedUrl, + + GetSourceRegion: getCopySnapshotSourceRegion, + + CopyInput: copyCopySnapshotInputForPresign, + SetDestinationRegion: setCopySnapshotdestinationRegion, - SetPresignedURL: setCopySnapshotPresignedUrl, + + SetPresignedURL: setCopySnapshotPresignedUrl, }, - Presigner: &presignAutoFillCopySnapshotClient{client: newCopySnapshotHTTPPresignURLClient(options)}, + Presigner: &presignAutoFillCopySnapshotClient{client: NewPresignClient(New(options))}, }) } type presignAutoFillCopySnapshotClient struct { - client *copySnapshotHTTPPresignURLClient + client *PresignClient } -func (c *presignAutoFillCopySnapshotClient) PresignURL(ctx context.Context, region string, params interface{}) (string, http.Header, error) { +// PresignURL is a middleware accessor that satisfies URLPresigner interface. +func (c *presignAutoFillCopySnapshotClient) PresignURL(ctx context.Context, srcRegion string, params interface{}) (*v4.PresignedHTTPRequest, error) { input, ok := params.(*CopySnapshotInput) if !ok { - return ``, nil, fmt.Errorf("expect *CopySnapshotInput type, got %T", params) + return nil, fmt.Errorf("expect *CopySnapshotInput type, got %T", params) } optFn := func(o *Options) { - o.Region = region + o.Region = srcRegion o.APIOptions = append(o.APIOptions, presignedurlcust.RemoveMiddleware) } - return c.client.PresignCopySnapshot(ctx, input, optFn) + presignOptFn := WithPresignClientFromClientOptions(optFn) + return c.client.PresignCopySnapshot(ctx, input, presignOptFn) } func newServiceMetadataMiddleware_opCopySnapshot(region string) *awsmiddleware.RegisterServiceMetadata { @@ -324,3 +281,35 @@ func newServiceMetadataMiddleware_opCopySnapshot(region string) *awsmiddleware.R OperationName: "CopySnapshot", } } + +// PresignCopySnapshot is used to generate a presigned HTTP Request which contains +// presigned URL, signed headers and HTTP method used. +func (c *PresignClient) PresignCopySnapshot(ctx context.Context, params *CopySnapshotInput, optFns ...func(*PresignOptions)) (*v4.PresignedHTTPRequest, error) { + if params == nil { + params = &CopySnapshotInput{} + } + var presignOptions PresignOptions + for _, fn := range optFns { + fn(&presignOptions) + } + if len(optFns) != 0 { + c = NewPresignClient(c.client, optFns...) + } + + clientOptFns := make([]func(o *Options), 0) + clientOptFns = append(clientOptFns, func(o *Options) { + o.HTTPClient = &smithyhttp.NopClient{} + }) + + ctx = presignedurlcust.WithIsPresigning(ctx) + result, _, err := c.client.invokeOperation(ctx, "CopySnapshot", params, clientOptFns, + addOperationCopySnapshotMiddlewares, + c.convertToPresignMiddleware, + ) + if err != nil { + return nil, err + } + + out := result.(*v4.PresignedHTTPRequest) + return out, nil +} diff --git a/service/internal/integrationtest/go.mod b/service/internal/integrationtest/go.mod index eac1fcefbfb..3b835ba95d0 100644 --- a/service/internal/integrationtest/go.mod +++ b/service/internal/integrationtest/go.mod @@ -87,6 +87,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/wafv2 v0.29.0 github.com/aws/aws-sdk-go-v2/service/workspaces v0.29.0 github.com/awslabs/smithy-go v0.3.1-0.20201120010914-c02b9493fe20 + github.com/google/go-cmp v0.5.2 ) go 1.15 diff --git a/service/internal/integrationtest/s3/presign_test.go b/service/internal/integrationtest/s3/presign_test.go new file mode 100644 index 00000000000..027329cd383 --- /dev/null +++ b/service/internal/integrationtest/s3/presign_test.go @@ -0,0 +1,156 @@ +// +build integration + +package s3 + +import ( + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "net/http" + "strconv" + "testing" + "time" + + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/aws/aws-sdk-go-v2/service/internal/integrationtest" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/google/go-cmp/cmp" +) + +func TestInteg_PresignURL(t *testing.T) { + cases := map[string]struct { + body io.Reader + expires time.Duration + sha256Header string + expectedSignedHeader http.Header + }{ + "standard": { + body: bytes.NewReader([]byte("Hello-world")), + expectedSignedHeader: http.Header{ + "content-type": {"application/octet-stream"}, + "content-length": {"11"}, + }, + }, + "nil-body": { + expectedSignedHeader: http.Header{}, + }, + "empty-body": { + body: bytes.NewReader([]byte("")), + expectedSignedHeader: http.Header{ + "content-type": {"application/octet-stream"}, + }, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + key := integrationtest.UniqueID() + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + cfg, err := integrationtest.LoadConfigWithDefaultRegion("us-west-2") + if err != nil { + t.Fatalf("failed to load config, %v", err) + } + + client := s3.NewFromConfig(cfg) + + // construct a put object + putObjectInput := &s3.PutObjectInput{ + Bucket: &setupMetadata.Buckets.Source.Name, + Key: &key, + Body: c.body, + } + + presignerClient := s3.NewPresignClient(client, func(options *s3.PresignOptions) { + options.Expires = 600 * time.Second + }) + + presignRequest, err := presignerClient.PresignPutObject(ctx, putObjectInput) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + for k, v := range c.expectedSignedHeader { + value := presignRequest.SignedHeader[k] + if len(value) == 0 { + t.Fatalf("expected %v header to be present in presigned url, got %v", k, presignRequest.SignedHeader) + } + + if diff := cmp.Diff(v, value); len(diff) != 0 { + t.Fatalf("expected %v header value to be %v got %v", k, v, value) + } + } + + resp, err := sendHTTPRequest(presignRequest, putObjectInput.Body) + if err != nil { + t.Errorf("expect no error while sending HTTP request using presigned url, got %v", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("failed to put S3 object, %d:%s", resp.StatusCode, resp.Status) + } + + // construct a get object + getObjectInput := &s3.GetObjectInput{ + Bucket: &setupMetadata.Buckets.Source.Name, + Key: &key, + } + + presignRequest, err = presignerClient.PresignGetObject(ctx, getObjectInput) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + + resp, err = sendHTTPRequest(presignRequest, nil) + if err != nil { + t.Errorf("expect no error while sending HTTP request using presigned url, got %v", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("failed to get S3 object, %d:%s", resp.StatusCode, resp.Status) + } + }) + } +} + +func sendHTTPRequest(presignRequest *v4.PresignedHTTPRequest, body io.Reader) (*http.Response, error) { + // create a http request + req, err := http.NewRequest(presignRequest.Method, presignRequest.URL, nil) + if err != nil { + return nil, fmt.Errorf("failed to build presigned request, %v", err) + } + + // assign the signed headers onto the http request + for k, vs := range presignRequest.SignedHeader { + for _, v := range vs { + req.Header.Add(k, v) + } + } + + // Need to ensure that the content length member is set of the HTTP Request + // or the request will NOT be transmitted correctly with a content length + // value across the wire. + if contLen := req.Header.Get("Content-Length"); len(contLen) > 0 { + req.ContentLength, _ = strconv.ParseInt(contLen, 10, 64) + } + + // assign the request body if not nil + if body != nil { + req.Body = ioutil.NopCloser(body) + if req.ContentLength == 0 { + req.Body = nil + } + } + + // Upload the object to S3. + resp, err := http.DefaultClient.Do(req) + return resp, err +} diff --git a/service/internal/presigned-url/middleware.go b/service/internal/presigned-url/middleware.go index 8caf0219600..f68be9ef643 100644 --- a/service/internal/presigned-url/middleware.go +++ b/service/internal/presigned-url/middleware.go @@ -3,34 +3,45 @@ package presignedurl import ( "context" "fmt" - "net/http" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/awslabs/smithy-go/middleware" ) +// URLPresigner provides the interface to presign the input parameters in to a +// presigned URL. +type URLPresigner interface { + // PresignURL presigns a URL. + PresignURL(ctx context.Context, srcRegion string, params interface{}) (*v4.PresignedHTTPRequest, error) +} + // ParameterAccessor provides an collection of accessor to for retrieving and // setting the values needed to PresignedURL generation type ParameterAccessor struct { + // GetPresignedURL accessor points to a function that retrieves a presigned url if present GetPresignedURL func(interface{}) (string, bool, error) + + // GetSourceRegion accessor points to a function that retrieves source region for presigned url GetSourceRegion func(interface{}) (string, bool, error) - CopyInput func(interface{}) (interface{}, error) + // CopyInput accessor points to a function that takes in an input, and returns a copy. + CopyInput func(interface{}) (interface{}, error) + + // SetDestinationRegion accessor points to a function that sets destination region on api input struct SetDestinationRegion func(interface{}, string) error - SetPresignedURL func(interface{}, string) error -} -// URLPresigner provides the interface to presign the input parameters in to a -// presigned URL. -type URLPresigner interface { - PresignURL(ctx context.Context, srcRegion string, params interface{}) ( - presignedURL string, signedHeader http.Header, err error, - ) + // SetPresignedURL accessor points to a function that sets presigned url on api input struct + SetPresignedURL func(interface{}, string) error } // Options provides the set of options needed by the presigned URL middleware. type Options struct { - Accessor ParameterAccessor + // Accessor are the parameter accessors used by this middleware + Accessor ParameterAccessor + + // Presigner is the URLPresigner used by the middleware Presigner URLPresigner } @@ -85,13 +96,13 @@ func (m *presign) HandleInitialize( return out, metadata, fmt.Errorf("presign middleware failed, %w", err) } - presignedURL, _, err := m.options.Presigner.PresignURL(ctx, srcRegion, paramCpy) + presignedReq, err := m.options.Presigner.PresignURL(ctx, srcRegion, paramCpy) if err != nil { return out, metadata, fmt.Errorf("unable to create presigned URL, %w", err) } // Update the original input with the presigned URL value. - if err = m.options.Accessor.SetPresignedURL(input.Parameters, presignedURL); err != nil { + if err = m.options.Accessor.SetPresignedURL(input.Parameters, presignedReq.URL); err != nil { return out, metadata, fmt.Errorf("presign middleware failed, %w", err) } diff --git a/service/internal/presigned-url/middleware_test.go b/service/internal/presigned-url/middleware_test.go index a81a5b1d83b..04c86a6a0b8 100644 --- a/service/internal/presigned-url/middleware_test.go +++ b/service/internal/presigned-url/middleware_test.go @@ -7,6 +7,8 @@ import ( "testing" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/awslabs/smithy-go/middleware" smithyhttp "github.com/awslabs/smithy-go/transport/http" "github.com/google/go-cmp/cmp" @@ -124,7 +126,7 @@ func getURLPresignMiddlewareOptions() Options { return nil }, }, - Presigner: mockURLPresigner{}, + Presigner: &mockURLPresigner{}, } } @@ -136,11 +138,14 @@ type mockURLPresignInput struct { type mockURLPresigner struct{} -func (mockURLPresigner) PresignURL(ctx context.Context, srcRegion string, params interface{}) ( - presignedURL string, signedHeader http.Header, err error, +func (*mockURLPresigner) PresignURL(ctx context.Context, srcRegion string, params interface{}) ( + req *v4.PresignedHTTPRequest, err error, ) { in := params.(*mockURLPresignInput) - return "https://example." + srcRegion + ".amazonaws.com/?DestinationRegion=" + in.DestinationRegion, - http.Header{}, nil + return &v4.PresignedHTTPRequest{ + URL: "https://example." + srcRegion + ".amazonaws.com/?DestinationRegion=" + in.DestinationRegion, + Method: "GET", + SignedHeader: http.Header{}, + }, nil } diff --git a/service/rds/api_client.go b/service/rds/api_client.go index 2be8c7cae95..cd36a219da5 100644 --- a/service/rds/api_client.go +++ b/service/rds/api_client.go @@ -6,6 +6,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/aws" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + "github.com/aws/aws-sdk-go-v2/aws/protocol/query" "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/aws/signer/v4" awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" @@ -226,3 +227,78 @@ func addRequestResponseLogging(stack *middleware.Stack, o Options) error { LogResponseWithBody: o.ClientLogMode.IsResponseWithBody(), }, middleware.After) } + +// HTTPPresignerV4 represents presigner interface used by presign url client +type HTTPPresignerV4 interface { + PresignHTTP( + ctx context.Context, credentials aws.Credentials, r *http.Request, + payloadHash string, service string, region string, signingTime time.Time, + ) (url string, signedHeader http.Header, err error) +} + +// PresignOptions represents the presign client options +type PresignOptions struct { + + // ClientOptions are list of functional options to mutate client options used by + // presign client + ClientOptions []func(*Options) + + // Presigner is the presigner used by the presign url client + Presigner HTTPPresignerV4 +} + +// WithPresignClientFromClientOptions is a helper utility to retrieve a function +// that takes PresignOption as input +func WithPresignClientFromClientOptions(optFns ...func(*Options)) func(*PresignOptions) { + return withPresignClientFromClientOptions(optFns).options +} + +type withPresignClientFromClientOptions []func(*Options) + +func (w withPresignClientFromClientOptions) options(o *PresignOptions) { + o.ClientOptions = append(o.ClientOptions, w...) +} + +// PresignClient represents the presign url client +type PresignClient struct { + client *Client + presigner HTTPPresignerV4 +} + +// NewPresignClient generates a presign client using provided API Client and +// presign options +func NewPresignClient(c *Client, optFns ...func(*PresignOptions)) *PresignClient { + var presignOptions PresignOptions + for _, fn := range optFns { + fn(&presignOptions) + } + client := copyAPIClient(c, presignOptions.ClientOptions...) + if presignOptions.Presigner == nil { + presignOptions.Presigner = v4.NewSigner() + } + + return &PresignClient{ + client: client, + presigner: presignOptions.Presigner, + } +} + +func copyAPIClient(c *Client, optFns ...func(*Options)) *Client { + return New(c.options, optFns...) +} + +func (c *PresignClient) convertToPresignMiddleware(stack *middleware.Stack, options Options) (err error) { + stack.Finalize.Clear() + stack.Deserialize.Clear() + stack.Build.Remove((*awsmiddleware.ClientRequestID)(nil).ID()) + err = stack.Finalize.Add(v4.NewPresignHTTPRequestMiddleware(options.Credentials, c.presigner), middleware.After) + if err != nil { + return err + } + // convert request to a GET request + err = query.AddAsGetRequestMiddleware(stack) + if err != nil { + return err + } + return nil +} diff --git a/service/rds/api_op_CopyDBClusterSnapshot.go b/service/rds/api_op_CopyDBClusterSnapshot.go index 441daece3b4..8662a7a11a8 100644 --- a/service/rds/api_op_CopyDBClusterSnapshot.go +++ b/service/rds/api_op_CopyDBClusterSnapshot.go @@ -6,13 +6,11 @@ import ( "context" "fmt" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" - "github.com/aws/aws-sdk-go-v2/aws/protocol/query" "github.com/aws/aws-sdk-go-v2/aws/signer/v4" presignedurlcust "github.com/aws/aws-sdk-go-v2/service/internal/presigned-url" "github.com/aws/aws-sdk-go-v2/service/rds/types" "github.com/awslabs/smithy-go/middleware" smithyhttp "github.com/awslabs/smithy-go/transport/http" - "net/http" ) // Copies a snapshot of a DB cluster. To copy a DB cluster snapshot from a shared @@ -330,80 +328,39 @@ func setCopyDBClusterSnapshotdestinationRegion(params interface{}, value string) input.destinationRegion = &value return nil } - -type copyDBClusterSnapshotHTTPPresignURLClient struct { - client *Client - presigner *v4.Signer -} - -func newCopyDBClusterSnapshotHTTPPresignURLClient(options Options, optFns ...func(*Options)) *copyDBClusterSnapshotHTTPPresignURLClient { - return ©DBClusterSnapshotHTTPPresignURLClient{ - client: New(options, optFns...), - presigner: v4.NewSigner(), - } -} -func (c *copyDBClusterSnapshotHTTPPresignURLClient) PresignCopyDBClusterSnapshot(ctx context.Context, params *CopyDBClusterSnapshotInput, optFns ...func(*Options)) (string, http.Header, error) { - if params == nil { - params = &CopyDBClusterSnapshotInput{} - } - - optFns = append(optFns, func(o *Options) { - o.HTTPClient = &smithyhttp.NopClient{} - }) - - ctx = presignedurlcust.WithIsPresigning(ctx) - result, _, err := c.client.invokeOperation(ctx, "CopyDBClusterSnapshot", params, optFns, - addOperationCopyDBClusterSnapshotMiddlewares, - c.convertToPresignMiddleware, - ) - if err != nil { - return ``, nil, err - } - - out := result.(*v4.PresignedHTTPRequest) - return out.URL, out.SignedHeader, nil -} -func (c *copyDBClusterSnapshotHTTPPresignURLClient) convertToPresignMiddleware(stack *middleware.Stack, options Options) (err error) { - stack.Finalize.Clear() - stack.Deserialize.Clear() - stack.Build.Remove((*awsmiddleware.ClientRequestID)(nil).ID()) - err = stack.Finalize.Add(v4.NewPresignHTTPRequestMiddleware(options.Credentials, c.presigner), middleware.After) - if err != nil { - return err - } - err = query.AddAsGetRequestMiddleware(stack) - if err != nil { - return err - } - return nil -} func addCopyDBClusterSnapshotPresignURLMiddleware(stack *middleware.Stack, options Options) error { return presignedurlcust.AddMiddleware(stack, presignedurlcust.Options{ Accessor: presignedurlcust.ParameterAccessor{ - GetPresignedURL: getCopyDBClusterSnapshotPreSignedUrl, - GetSourceRegion: getCopyDBClusterSnapshotSourceRegion, - CopyInput: copyCopyDBClusterSnapshotInputForPresign, + GetPresignedURL: getCopyDBClusterSnapshotPreSignedUrl, + + GetSourceRegion: getCopyDBClusterSnapshotSourceRegion, + + CopyInput: copyCopyDBClusterSnapshotInputForPresign, + SetDestinationRegion: setCopyDBClusterSnapshotdestinationRegion, - SetPresignedURL: setCopyDBClusterSnapshotPreSignedUrl, + + SetPresignedURL: setCopyDBClusterSnapshotPreSignedUrl, }, - Presigner: &presignAutoFillCopyDBClusterSnapshotClient{client: newCopyDBClusterSnapshotHTTPPresignURLClient(options)}, + Presigner: &presignAutoFillCopyDBClusterSnapshotClient{client: NewPresignClient(New(options))}, }) } type presignAutoFillCopyDBClusterSnapshotClient struct { - client *copyDBClusterSnapshotHTTPPresignURLClient + client *PresignClient } -func (c *presignAutoFillCopyDBClusterSnapshotClient) PresignURL(ctx context.Context, region string, params interface{}) (string, http.Header, error) { +// PresignURL is a middleware accessor that satisfies URLPresigner interface. +func (c *presignAutoFillCopyDBClusterSnapshotClient) PresignURL(ctx context.Context, srcRegion string, params interface{}) (*v4.PresignedHTTPRequest, error) { input, ok := params.(*CopyDBClusterSnapshotInput) if !ok { - return ``, nil, fmt.Errorf("expect *CopyDBClusterSnapshotInput type, got %T", params) + return nil, fmt.Errorf("expect *CopyDBClusterSnapshotInput type, got %T", params) } optFn := func(o *Options) { - o.Region = region + o.Region = srcRegion o.APIOptions = append(o.APIOptions, presignedurlcust.RemoveMiddleware) } - return c.client.PresignCopyDBClusterSnapshot(ctx, input, optFn) + presignOptFn := WithPresignClientFromClientOptions(optFn) + return c.client.PresignCopyDBClusterSnapshot(ctx, input, presignOptFn) } func newServiceMetadataMiddleware_opCopyDBClusterSnapshot(region string) *awsmiddleware.RegisterServiceMetadata { @@ -414,3 +371,35 @@ func newServiceMetadataMiddleware_opCopyDBClusterSnapshot(region string) *awsmid OperationName: "CopyDBClusterSnapshot", } } + +// PresignCopyDBClusterSnapshot is used to generate a presigned HTTP Request which +// contains presigned URL, signed headers and HTTP method used. +func (c *PresignClient) PresignCopyDBClusterSnapshot(ctx context.Context, params *CopyDBClusterSnapshotInput, optFns ...func(*PresignOptions)) (*v4.PresignedHTTPRequest, error) { + if params == nil { + params = &CopyDBClusterSnapshotInput{} + } + var presignOptions PresignOptions + for _, fn := range optFns { + fn(&presignOptions) + } + if len(optFns) != 0 { + c = NewPresignClient(c.client, optFns...) + } + + clientOptFns := make([]func(o *Options), 0) + clientOptFns = append(clientOptFns, func(o *Options) { + o.HTTPClient = &smithyhttp.NopClient{} + }) + + ctx = presignedurlcust.WithIsPresigning(ctx) + result, _, err := c.client.invokeOperation(ctx, "CopyDBClusterSnapshot", params, clientOptFns, + addOperationCopyDBClusterSnapshotMiddlewares, + c.convertToPresignMiddleware, + ) + if err != nil { + return nil, err + } + + out := result.(*v4.PresignedHTTPRequest) + return out, nil +} diff --git a/service/rds/api_op_CopyDBSnapshot.go b/service/rds/api_op_CopyDBSnapshot.go index 8f1fe711d46..a839500bc76 100644 --- a/service/rds/api_op_CopyDBSnapshot.go +++ b/service/rds/api_op_CopyDBSnapshot.go @@ -6,13 +6,11 @@ import ( "context" "fmt" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" - "github.com/aws/aws-sdk-go-v2/aws/protocol/query" "github.com/aws/aws-sdk-go-v2/aws/signer/v4" presignedurlcust "github.com/aws/aws-sdk-go-v2/service/internal/presigned-url" "github.com/aws/aws-sdk-go-v2/service/rds/types" "github.com/awslabs/smithy-go/middleware" smithyhttp "github.com/awslabs/smithy-go/transport/http" - "net/http" ) // Copies the specified DB snapshot. The source DB snapshot must be in the @@ -283,80 +281,39 @@ func setCopyDBSnapshotdestinationRegion(params interface{}, value string) error input.destinationRegion = &value return nil } - -type copyDBSnapshotHTTPPresignURLClient struct { - client *Client - presigner *v4.Signer -} - -func newCopyDBSnapshotHTTPPresignURLClient(options Options, optFns ...func(*Options)) *copyDBSnapshotHTTPPresignURLClient { - return ©DBSnapshotHTTPPresignURLClient{ - client: New(options, optFns...), - presigner: v4.NewSigner(), - } -} -func (c *copyDBSnapshotHTTPPresignURLClient) PresignCopyDBSnapshot(ctx context.Context, params *CopyDBSnapshotInput, optFns ...func(*Options)) (string, http.Header, error) { - if params == nil { - params = &CopyDBSnapshotInput{} - } - - optFns = append(optFns, func(o *Options) { - o.HTTPClient = &smithyhttp.NopClient{} - }) - - ctx = presignedurlcust.WithIsPresigning(ctx) - result, _, err := c.client.invokeOperation(ctx, "CopyDBSnapshot", params, optFns, - addOperationCopyDBSnapshotMiddlewares, - c.convertToPresignMiddleware, - ) - if err != nil { - return ``, nil, err - } - - out := result.(*v4.PresignedHTTPRequest) - return out.URL, out.SignedHeader, nil -} -func (c *copyDBSnapshotHTTPPresignURLClient) convertToPresignMiddleware(stack *middleware.Stack, options Options) (err error) { - stack.Finalize.Clear() - stack.Deserialize.Clear() - stack.Build.Remove((*awsmiddleware.ClientRequestID)(nil).ID()) - err = stack.Finalize.Add(v4.NewPresignHTTPRequestMiddleware(options.Credentials, c.presigner), middleware.After) - if err != nil { - return err - } - err = query.AddAsGetRequestMiddleware(stack) - if err != nil { - return err - } - return nil -} func addCopyDBSnapshotPresignURLMiddleware(stack *middleware.Stack, options Options) error { return presignedurlcust.AddMiddleware(stack, presignedurlcust.Options{ Accessor: presignedurlcust.ParameterAccessor{ - GetPresignedURL: getCopyDBSnapshotPreSignedUrl, - GetSourceRegion: getCopyDBSnapshotSourceRegion, - CopyInput: copyCopyDBSnapshotInputForPresign, + GetPresignedURL: getCopyDBSnapshotPreSignedUrl, + + GetSourceRegion: getCopyDBSnapshotSourceRegion, + + CopyInput: copyCopyDBSnapshotInputForPresign, + SetDestinationRegion: setCopyDBSnapshotdestinationRegion, - SetPresignedURL: setCopyDBSnapshotPreSignedUrl, + + SetPresignedURL: setCopyDBSnapshotPreSignedUrl, }, - Presigner: &presignAutoFillCopyDBSnapshotClient{client: newCopyDBSnapshotHTTPPresignURLClient(options)}, + Presigner: &presignAutoFillCopyDBSnapshotClient{client: NewPresignClient(New(options))}, }) } type presignAutoFillCopyDBSnapshotClient struct { - client *copyDBSnapshotHTTPPresignURLClient + client *PresignClient } -func (c *presignAutoFillCopyDBSnapshotClient) PresignURL(ctx context.Context, region string, params interface{}) (string, http.Header, error) { +// PresignURL is a middleware accessor that satisfies URLPresigner interface. +func (c *presignAutoFillCopyDBSnapshotClient) PresignURL(ctx context.Context, srcRegion string, params interface{}) (*v4.PresignedHTTPRequest, error) { input, ok := params.(*CopyDBSnapshotInput) if !ok { - return ``, nil, fmt.Errorf("expect *CopyDBSnapshotInput type, got %T", params) + return nil, fmt.Errorf("expect *CopyDBSnapshotInput type, got %T", params) } optFn := func(o *Options) { - o.Region = region + o.Region = srcRegion o.APIOptions = append(o.APIOptions, presignedurlcust.RemoveMiddleware) } - return c.client.PresignCopyDBSnapshot(ctx, input, optFn) + presignOptFn := WithPresignClientFromClientOptions(optFn) + return c.client.PresignCopyDBSnapshot(ctx, input, presignOptFn) } func newServiceMetadataMiddleware_opCopyDBSnapshot(region string) *awsmiddleware.RegisterServiceMetadata { @@ -367,3 +324,35 @@ func newServiceMetadataMiddleware_opCopyDBSnapshot(region string) *awsmiddleware OperationName: "CopyDBSnapshot", } } + +// PresignCopyDBSnapshot is used to generate a presigned HTTP Request which +// contains presigned URL, signed headers and HTTP method used. +func (c *PresignClient) PresignCopyDBSnapshot(ctx context.Context, params *CopyDBSnapshotInput, optFns ...func(*PresignOptions)) (*v4.PresignedHTTPRequest, error) { + if params == nil { + params = &CopyDBSnapshotInput{} + } + var presignOptions PresignOptions + for _, fn := range optFns { + fn(&presignOptions) + } + if len(optFns) != 0 { + c = NewPresignClient(c.client, optFns...) + } + + clientOptFns := make([]func(o *Options), 0) + clientOptFns = append(clientOptFns, func(o *Options) { + o.HTTPClient = &smithyhttp.NopClient{} + }) + + ctx = presignedurlcust.WithIsPresigning(ctx) + result, _, err := c.client.invokeOperation(ctx, "CopyDBSnapshot", params, clientOptFns, + addOperationCopyDBSnapshotMiddlewares, + c.convertToPresignMiddleware, + ) + if err != nil { + return nil, err + } + + out := result.(*v4.PresignedHTTPRequest) + return out, nil +} diff --git a/service/rds/api_op_CreateDBCluster.go b/service/rds/api_op_CreateDBCluster.go index 85d3f03016d..78e89d9ed0f 100644 --- a/service/rds/api_op_CreateDBCluster.go +++ b/service/rds/api_op_CreateDBCluster.go @@ -6,13 +6,11 @@ import ( "context" "fmt" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" - "github.com/aws/aws-sdk-go-v2/aws/protocol/query" "github.com/aws/aws-sdk-go-v2/aws/signer/v4" presignedurlcust "github.com/aws/aws-sdk-go-v2/service/internal/presigned-url" "github.com/aws/aws-sdk-go-v2/service/rds/types" "github.com/awslabs/smithy-go/middleware" smithyhttp "github.com/awslabs/smithy-go/transport/http" - "net/http" ) // Creates a new Amazon Aurora DB cluster. You can use the @@ -467,80 +465,39 @@ func setCreateDBClusterdestinationRegion(params interface{}, value string) error input.destinationRegion = &value return nil } - -type createDBClusterHTTPPresignURLClient struct { - client *Client - presigner *v4.Signer -} - -func newCreateDBClusterHTTPPresignURLClient(options Options, optFns ...func(*Options)) *createDBClusterHTTPPresignURLClient { - return &createDBClusterHTTPPresignURLClient{ - client: New(options, optFns...), - presigner: v4.NewSigner(), - } -} -func (c *createDBClusterHTTPPresignURLClient) PresignCreateDBCluster(ctx context.Context, params *CreateDBClusterInput, optFns ...func(*Options)) (string, http.Header, error) { - if params == nil { - params = &CreateDBClusterInput{} - } - - optFns = append(optFns, func(o *Options) { - o.HTTPClient = &smithyhttp.NopClient{} - }) - - ctx = presignedurlcust.WithIsPresigning(ctx) - result, _, err := c.client.invokeOperation(ctx, "CreateDBCluster", params, optFns, - addOperationCreateDBClusterMiddlewares, - c.convertToPresignMiddleware, - ) - if err != nil { - return ``, nil, err - } - - out := result.(*v4.PresignedHTTPRequest) - return out.URL, out.SignedHeader, nil -} -func (c *createDBClusterHTTPPresignURLClient) convertToPresignMiddleware(stack *middleware.Stack, options Options) (err error) { - stack.Finalize.Clear() - stack.Deserialize.Clear() - stack.Build.Remove((*awsmiddleware.ClientRequestID)(nil).ID()) - err = stack.Finalize.Add(v4.NewPresignHTTPRequestMiddleware(options.Credentials, c.presigner), middleware.After) - if err != nil { - return err - } - err = query.AddAsGetRequestMiddleware(stack) - if err != nil { - return err - } - return nil -} func addCreateDBClusterPresignURLMiddleware(stack *middleware.Stack, options Options) error { return presignedurlcust.AddMiddleware(stack, presignedurlcust.Options{ Accessor: presignedurlcust.ParameterAccessor{ - GetPresignedURL: getCreateDBClusterPreSignedUrl, - GetSourceRegion: getCreateDBClusterSourceRegion, - CopyInput: copyCreateDBClusterInputForPresign, + GetPresignedURL: getCreateDBClusterPreSignedUrl, + + GetSourceRegion: getCreateDBClusterSourceRegion, + + CopyInput: copyCreateDBClusterInputForPresign, + SetDestinationRegion: setCreateDBClusterdestinationRegion, - SetPresignedURL: setCreateDBClusterPreSignedUrl, + + SetPresignedURL: setCreateDBClusterPreSignedUrl, }, - Presigner: &presignAutoFillCreateDBClusterClient{client: newCreateDBClusterHTTPPresignURLClient(options)}, + Presigner: &presignAutoFillCreateDBClusterClient{client: NewPresignClient(New(options))}, }) } type presignAutoFillCreateDBClusterClient struct { - client *createDBClusterHTTPPresignURLClient + client *PresignClient } -func (c *presignAutoFillCreateDBClusterClient) PresignURL(ctx context.Context, region string, params interface{}) (string, http.Header, error) { +// PresignURL is a middleware accessor that satisfies URLPresigner interface. +func (c *presignAutoFillCreateDBClusterClient) PresignURL(ctx context.Context, srcRegion string, params interface{}) (*v4.PresignedHTTPRequest, error) { input, ok := params.(*CreateDBClusterInput) if !ok { - return ``, nil, fmt.Errorf("expect *CreateDBClusterInput type, got %T", params) + return nil, fmt.Errorf("expect *CreateDBClusterInput type, got %T", params) } optFn := func(o *Options) { - o.Region = region + o.Region = srcRegion o.APIOptions = append(o.APIOptions, presignedurlcust.RemoveMiddleware) } - return c.client.PresignCreateDBCluster(ctx, input, optFn) + presignOptFn := WithPresignClientFromClientOptions(optFn) + return c.client.PresignCreateDBCluster(ctx, input, presignOptFn) } func newServiceMetadataMiddleware_opCreateDBCluster(region string) *awsmiddleware.RegisterServiceMetadata { @@ -551,3 +508,35 @@ func newServiceMetadataMiddleware_opCreateDBCluster(region string) *awsmiddlewar OperationName: "CreateDBCluster", } } + +// PresignCreateDBCluster is used to generate a presigned HTTP Request which +// contains presigned URL, signed headers and HTTP method used. +func (c *PresignClient) PresignCreateDBCluster(ctx context.Context, params *CreateDBClusterInput, optFns ...func(*PresignOptions)) (*v4.PresignedHTTPRequest, error) { + if params == nil { + params = &CreateDBClusterInput{} + } + var presignOptions PresignOptions + for _, fn := range optFns { + fn(&presignOptions) + } + if len(optFns) != 0 { + c = NewPresignClient(c.client, optFns...) + } + + clientOptFns := make([]func(o *Options), 0) + clientOptFns = append(clientOptFns, func(o *Options) { + o.HTTPClient = &smithyhttp.NopClient{} + }) + + ctx = presignedurlcust.WithIsPresigning(ctx) + result, _, err := c.client.invokeOperation(ctx, "CreateDBCluster", params, clientOptFns, + addOperationCreateDBClusterMiddlewares, + c.convertToPresignMiddleware, + ) + if err != nil { + return nil, err + } + + out := result.(*v4.PresignedHTTPRequest) + return out, nil +} diff --git a/service/rds/api_op_CreateDBInstanceReadReplica.go b/service/rds/api_op_CreateDBInstanceReadReplica.go index b995932f4c6..5fef4758eba 100644 --- a/service/rds/api_op_CreateDBInstanceReadReplica.go +++ b/service/rds/api_op_CreateDBInstanceReadReplica.go @@ -6,13 +6,11 @@ import ( "context" "fmt" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" - "github.com/aws/aws-sdk-go-v2/aws/protocol/query" "github.com/aws/aws-sdk-go-v2/aws/signer/v4" presignedurlcust "github.com/aws/aws-sdk-go-v2/service/internal/presigned-url" "github.com/aws/aws-sdk-go-v2/service/rds/types" "github.com/awslabs/smithy-go/middleware" smithyhttp "github.com/awslabs/smithy-go/transport/http" - "net/http" ) // Creates a new DB instance that acts as a read replica for an existing source DB @@ -477,80 +475,39 @@ func setCreateDBInstanceReadReplicadestinationRegion(params interface{}, value s input.destinationRegion = &value return nil } - -type createDBInstanceReadReplicaHTTPPresignURLClient struct { - client *Client - presigner *v4.Signer -} - -func newCreateDBInstanceReadReplicaHTTPPresignURLClient(options Options, optFns ...func(*Options)) *createDBInstanceReadReplicaHTTPPresignURLClient { - return &createDBInstanceReadReplicaHTTPPresignURLClient{ - client: New(options, optFns...), - presigner: v4.NewSigner(), - } -} -func (c *createDBInstanceReadReplicaHTTPPresignURLClient) PresignCreateDBInstanceReadReplica(ctx context.Context, params *CreateDBInstanceReadReplicaInput, optFns ...func(*Options)) (string, http.Header, error) { - if params == nil { - params = &CreateDBInstanceReadReplicaInput{} - } - - optFns = append(optFns, func(o *Options) { - o.HTTPClient = &smithyhttp.NopClient{} - }) - - ctx = presignedurlcust.WithIsPresigning(ctx) - result, _, err := c.client.invokeOperation(ctx, "CreateDBInstanceReadReplica", params, optFns, - addOperationCreateDBInstanceReadReplicaMiddlewares, - c.convertToPresignMiddleware, - ) - if err != nil { - return ``, nil, err - } - - out := result.(*v4.PresignedHTTPRequest) - return out.URL, out.SignedHeader, nil -} -func (c *createDBInstanceReadReplicaHTTPPresignURLClient) convertToPresignMiddleware(stack *middleware.Stack, options Options) (err error) { - stack.Finalize.Clear() - stack.Deserialize.Clear() - stack.Build.Remove((*awsmiddleware.ClientRequestID)(nil).ID()) - err = stack.Finalize.Add(v4.NewPresignHTTPRequestMiddleware(options.Credentials, c.presigner), middleware.After) - if err != nil { - return err - } - err = query.AddAsGetRequestMiddleware(stack) - if err != nil { - return err - } - return nil -} func addCreateDBInstanceReadReplicaPresignURLMiddleware(stack *middleware.Stack, options Options) error { return presignedurlcust.AddMiddleware(stack, presignedurlcust.Options{ Accessor: presignedurlcust.ParameterAccessor{ - GetPresignedURL: getCreateDBInstanceReadReplicaPreSignedUrl, - GetSourceRegion: getCreateDBInstanceReadReplicaSourceRegion, - CopyInput: copyCreateDBInstanceReadReplicaInputForPresign, + GetPresignedURL: getCreateDBInstanceReadReplicaPreSignedUrl, + + GetSourceRegion: getCreateDBInstanceReadReplicaSourceRegion, + + CopyInput: copyCreateDBInstanceReadReplicaInputForPresign, + SetDestinationRegion: setCreateDBInstanceReadReplicadestinationRegion, - SetPresignedURL: setCreateDBInstanceReadReplicaPreSignedUrl, + + SetPresignedURL: setCreateDBInstanceReadReplicaPreSignedUrl, }, - Presigner: &presignAutoFillCreateDBInstanceReadReplicaClient{client: newCreateDBInstanceReadReplicaHTTPPresignURLClient(options)}, + Presigner: &presignAutoFillCreateDBInstanceReadReplicaClient{client: NewPresignClient(New(options))}, }) } type presignAutoFillCreateDBInstanceReadReplicaClient struct { - client *createDBInstanceReadReplicaHTTPPresignURLClient + client *PresignClient } -func (c *presignAutoFillCreateDBInstanceReadReplicaClient) PresignURL(ctx context.Context, region string, params interface{}) (string, http.Header, error) { +// PresignURL is a middleware accessor that satisfies URLPresigner interface. +func (c *presignAutoFillCreateDBInstanceReadReplicaClient) PresignURL(ctx context.Context, srcRegion string, params interface{}) (*v4.PresignedHTTPRequest, error) { input, ok := params.(*CreateDBInstanceReadReplicaInput) if !ok { - return ``, nil, fmt.Errorf("expect *CreateDBInstanceReadReplicaInput type, got %T", params) + return nil, fmt.Errorf("expect *CreateDBInstanceReadReplicaInput type, got %T", params) } optFn := func(o *Options) { - o.Region = region + o.Region = srcRegion o.APIOptions = append(o.APIOptions, presignedurlcust.RemoveMiddleware) } - return c.client.PresignCreateDBInstanceReadReplica(ctx, input, optFn) + presignOptFn := WithPresignClientFromClientOptions(optFn) + return c.client.PresignCreateDBInstanceReadReplica(ctx, input, presignOptFn) } func newServiceMetadataMiddleware_opCreateDBInstanceReadReplica(region string) *awsmiddleware.RegisterServiceMetadata { @@ -561,3 +518,35 @@ func newServiceMetadataMiddleware_opCreateDBInstanceReadReplica(region string) * OperationName: "CreateDBInstanceReadReplica", } } + +// PresignCreateDBInstanceReadReplica is used to generate a presigned HTTP Request +// which contains presigned URL, signed headers and HTTP method used. +func (c *PresignClient) PresignCreateDBInstanceReadReplica(ctx context.Context, params *CreateDBInstanceReadReplicaInput, optFns ...func(*PresignOptions)) (*v4.PresignedHTTPRequest, error) { + if params == nil { + params = &CreateDBInstanceReadReplicaInput{} + } + var presignOptions PresignOptions + for _, fn := range optFns { + fn(&presignOptions) + } + if len(optFns) != 0 { + c = NewPresignClient(c.client, optFns...) + } + + clientOptFns := make([]func(o *Options), 0) + clientOptFns = append(clientOptFns, func(o *Options) { + o.HTTPClient = &smithyhttp.NopClient{} + }) + + ctx = presignedurlcust.WithIsPresigning(ctx) + result, _, err := c.client.invokeOperation(ctx, "CreateDBInstanceReadReplica", params, clientOptFns, + addOperationCreateDBInstanceReadReplicaMiddlewares, + c.convertToPresignMiddleware, + ) + if err != nil { + return nil, err + } + + out := result.(*v4.PresignedHTTPRequest) + return out, nil +} diff --git a/service/rds/go.sum b/service/rds/go.sum index d39b60a4034..26564e07dc7 100644 --- a/service/rds/go.sum +++ b/service/rds/go.sum @@ -1,3 +1,10 @@ +github.com/aws/aws-sdk-go-v2 v0.28.0/go.mod h1:P9h1Cf+uOpElAT533QXKOzrpFaOlm8JMorThJNUfQ6Q= +github.com/aws/aws-sdk-go-v2 v0.29.0 h1:V/KKvuMO2hwHRg2SXJc5aasBHhD1AWbS6KMWg/Ueq1w= +github.com/aws/aws-sdk-go-v2 v0.29.0/go.mod h1:4d1/Ee0vCwCF7BfG1hCT3zu82493cRy5+VZ8JHvMPf0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v0.1.0 h1:r6FAnK2190ahYhtzj0HRcxCWlb7SE9/zI8v73npLkYU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v0.1.0/go.mod h1:aLwdZO0CArC1N9dbBxt7C2PBxkPktM0sDsr9iS7A9SY= +github.com/awslabs/smithy-go v0.2.1/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= +github.com/awslabs/smithy-go v0.3.0/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= github.com/awslabs/smithy-go v0.3.1-0.20201104233911-38864709e183/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= github.com/awslabs/smithy-go v0.3.1-0.20201108010311-62c2a93810b4 h1:Aj5dOF+lDoEhU92no7YZF0IokuWGjiNrcm/DGIG3iII= github.com/awslabs/smithy-go v0.3.1-0.20201108010311-62c2a93810b4/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= @@ -12,5 +19,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/service/s3/api_client.go b/service/s3/api_client.go index 2825f4011d6..b52d80b46d0 100644 --- a/service/s3/api_client.go +++ b/service/s3/api_client.go @@ -11,6 +11,7 @@ import ( awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" acceptencodingcust "github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding" "github.com/aws/aws-sdk-go-v2/service/internal/s3shared" + s3cust "github.com/aws/aws-sdk-go-v2/service/s3/internal/customizations" smithy "github.com/awslabs/smithy-go" "github.com/awslabs/smithy-go/logging" "github.com/awslabs/smithy-go/middleware" @@ -258,3 +259,98 @@ func addRequestResponseLogging(stack *middleware.Stack, o Options) error { LogResponseWithBody: o.ClientLogMode.IsResponseWithBody(), }, middleware.After) } + +// HTTPPresignerV4 represents presigner interface used by presign url client +type HTTPPresignerV4 interface { + PresignHTTP( + ctx context.Context, credentials aws.Credentials, r *http.Request, + payloadHash string, service string, region string, signingTime time.Time, + ) (url string, signedHeader http.Header, err error) +} + +// PresignOptions represents the presign client options +type PresignOptions struct { + + // ClientOptions are list of functional options to mutate client options used by + // presign client + ClientOptions []func(*Options) + + // Presigner is the presigner used by the presign url client + Presigner HTTPPresignerV4 + + // Expires sets the expiration duration for the generated presign url. This should + // be the duration in seconds the presigned URL should be considered valid for. If + // not set or set to zero, presign url would default to expire after 900 seconds. + Expires time.Duration +} + +// WithPresignClientFromClientOptions is a helper utility to retrieve a function +// that takes PresignOption as input +func WithPresignClientFromClientOptions(optFns ...func(*Options)) func(*PresignOptions) { + return withPresignClientFromClientOptions(optFns).options +} + +type withPresignClientFromClientOptions []func(*Options) + +func (w withPresignClientFromClientOptions) options(o *PresignOptions) { + o.ClientOptions = append(o.ClientOptions, w...) +} + +// WithPresignExpires is a helper utility to append Expires value on presign +// options optional function +func WithPresignExpires(dur time.Duration) func(*PresignOptions) { + return withPresignExpires(dur).options +} + +type withPresignExpires time.Duration + +func (w withPresignExpires) options(o *PresignOptions) { + o.Expires = time.Duration(w) +} + +// PresignClient represents the presign url client +type PresignClient struct { + client *Client + presigner HTTPPresignerV4 + expires time.Duration +} + +// NewPresignClient generates a presign client using provided API Client and +// presign options +func NewPresignClient(c *Client, optFns ...func(*PresignOptions)) *PresignClient { + var presignOptions PresignOptions + for _, fn := range optFns { + fn(&presignOptions) + } + client := copyAPIClient(c, presignOptions.ClientOptions...) + if presignOptions.Presigner == nil { + presignOptions.Presigner = v4.NewSigner() + } + + return &PresignClient{ + client: client, + presigner: presignOptions.Presigner, + expires: presignOptions.Expires, + } +} + +func copyAPIClient(c *Client, optFns ...func(*Options)) *Client { + return New(c.options, optFns...) +} + +func (c *PresignClient) convertToPresignMiddleware(stack *middleware.Stack, options Options) (err error) { + stack.Finalize.Clear() + stack.Deserialize.Clear() + stack.Build.Remove((*awsmiddleware.ClientRequestID)(nil).ID()) + err = stack.Finalize.Add(v4.NewPresignHTTPRequestMiddleware(options.Credentials, c.presigner), middleware.After) + if err != nil { + return err + } + // add middleware to set expiration for s3 presigned url, if expiration is set to + // 0, this middleware sets a default expiration of 900 seconds + err = stack.Build.Add(&s3cust.AddExpiresOnPresignedURL{Expires: c.expires}, middleware.After) + if err != nil { + return err + } + return nil +} diff --git a/service/s3/api_op_GetObject.go b/service/s3/api_op_GetObject.go index 76a279a0d6d..cb641f6ef04 100644 --- a/service/s3/api_op_GetObject.go +++ b/service/s3/api_op_GetObject.go @@ -6,6 +6,7 @@ import ( "context" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + presignedurlcust "github.com/aws/aws-sdk-go-v2/service/internal/presigned-url" s3cust "github.com/aws/aws-sdk-go-v2/service/s3/internal/customizations" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/awslabs/smithy-go/middleware" @@ -473,3 +474,35 @@ func addGetObjectUpdateEndpoint(stack *middleware.Stack, options Options) error UseARNRegion: options.UseARNRegion, }) } + +// PresignGetObject is used to generate a presigned HTTP Request which contains +// presigned URL, signed headers and HTTP method used. +func (c *PresignClient) PresignGetObject(ctx context.Context, params *GetObjectInput, optFns ...func(*PresignOptions)) (*v4.PresignedHTTPRequest, error) { + if params == nil { + params = &GetObjectInput{} + } + var presignOptions PresignOptions + for _, fn := range optFns { + fn(&presignOptions) + } + if len(optFns) != 0 { + c = NewPresignClient(c.client, optFns...) + } + + clientOptFns := make([]func(o *Options), 0) + clientOptFns = append(clientOptFns, func(o *Options) { + o.HTTPClient = &smithyhttp.NopClient{} + }) + + ctx = presignedurlcust.WithIsPresigning(ctx) + result, _, err := c.client.invokeOperation(ctx, "GetObject", params, clientOptFns, + addOperationGetObjectMiddlewares, + c.convertToPresignMiddleware, + ) + if err != nil { + return nil, err + } + + out := result.(*v4.PresignedHTTPRequest) + return out, nil +} diff --git a/service/s3/api_op_PutObject.go b/service/s3/api_op_PutObject.go index 65a55f775ee..8e5f4c92723 100644 --- a/service/s3/api_op_PutObject.go +++ b/service/s3/api_op_PutObject.go @@ -6,6 +6,7 @@ import ( "context" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + presignedurlcust "github.com/aws/aws-sdk-go-v2/service/internal/presigned-url" s3cust "github.com/aws/aws-sdk-go-v2/service/s3/internal/customizations" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/awslabs/smithy-go/middleware" @@ -424,3 +425,42 @@ func addPutObjectUpdateEndpoint(stack *middleware.Stack, options Options) error UseARNRegion: options.UseARNRegion, }) } + +// PresignPutObject is used to generate a presigned HTTP Request which contains +// presigned URL, signed headers and HTTP method used. +func (c *PresignClient) PresignPutObject(ctx context.Context, params *PutObjectInput, optFns ...func(*PresignOptions)) (*v4.PresignedHTTPRequest, error) { + if params == nil { + params = &PutObjectInput{} + } + var presignOptions PresignOptions + for _, fn := range optFns { + fn(&presignOptions) + } + if len(optFns) != 0 { + c = NewPresignClient(c.client, optFns...) + } + + clientOptFns := make([]func(o *Options), 0) + clientOptFns = append(clientOptFns, func(o *Options) { + o.HTTPClient = &smithyhttp.NopClient{} + }) + + ctx = presignedurlcust.WithIsPresigning(ctx) + result, _, err := c.client.invokeOperation(ctx, "PutObject", params, clientOptFns, + addOperationPutObjectMiddlewares, + c.convertToPresignMiddleware, + addPutObjectPayloadAsUnsigned, + ) + if err != nil { + return nil, err + } + + out := result.(*v4.PresignedHTTPRequest) + return out, nil +} + +func addPutObjectPayloadAsUnsigned(stack *middleware.Stack, options Options) error { + v4.RemoveContentSHA256HeaderMiddleware(stack) + v4.RemoveComputePayloadSHA256Middleware(stack) + return v4.AddUnsignedPayloadMiddleware(stack) +} diff --git a/service/s3/go.mod b/service/s3/go.mod index 5882d405dd2..1b09d08a705 100644 --- a/service/s3/go.mod +++ b/service/s3/go.mod @@ -5,12 +5,16 @@ go 1.15 require ( github.com/aws/aws-sdk-go-v2 v0.29.1-0.20201113222241-726e4a15683d github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v0.3.1-0.20201113222241-726e4a15683d + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v0.1.1-0.20201113222241-726e4a15683d github.com/aws/aws-sdk-go-v2/service/internal/s3shared v0.3.1-0.20201113222241-726e4a15683d github.com/awslabs/smithy-go v0.3.1-0.20201108010311-62c2a93810b4 + github.com/google/go-cmp v0.5.2 ) replace github.com/aws/aws-sdk-go-v2 => ../../ replace github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding => ../../service/internal/accept-encoding/ +replace github.com/aws/aws-sdk-go-v2/service/internal/presigned-url => ../../service/internal/presigned-url/ + replace github.com/aws/aws-sdk-go-v2/service/internal/s3shared => ../../service/internal/s3shared/ diff --git a/service/s3/go.sum b/service/s3/go.sum index 57ffc83dc1c..e1dc3ec43bf 100644 --- a/service/s3/go.sum +++ b/service/s3/go.sum @@ -5,6 +5,8 @@ github.com/awslabs/smithy-go v0.3.1-0.20201108010311-62c2a93810b4/go.mod h1:hPOQ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/service/s3/internal/configtesting/go.mod b/service/s3/internal/configtesting/go.mod index 306d989e686..7e5f425a344 100644 --- a/service/s3/internal/configtesting/go.mod +++ b/service/s3/internal/configtesting/go.mod @@ -22,3 +22,5 @@ replace github.com/aws/aws-sdk-go-v2/service/internal/s3shared => ../../../../se replace github.com/aws/aws-sdk-go-v2/service/sts => ../../../../service/sts/ replace github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding => ../../../../service/internal/accept-encoding/ + +replace github.com/aws/aws-sdk-go-v2/service/internal/presigned-url => ../../../../service/internal/presigned-url/ diff --git a/service/s3/internal/configtesting/go.sum b/service/s3/internal/configtesting/go.sum index 57ffc83dc1c..e1dc3ec43bf 100644 --- a/service/s3/internal/configtesting/go.sum +++ b/service/s3/internal/configtesting/go.sum @@ -5,6 +5,8 @@ github.com/awslabs/smithy-go v0.3.1-0.20201108010311-62c2a93810b4/go.mod h1:hPOQ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/service/s3/internal/customizations/presign_test.go b/service/s3/internal/customizations/presign_test.go new file mode 100644 index 00000000000..f67df2f5d5e --- /dev/null +++ b/service/s3/internal/customizations/presign_test.go @@ -0,0 +1,180 @@ +package customizations_test + +import ( + "bytes" + "context" + "net/http" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/internal/awstesting/unit" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +func TestPutObject_PresignURL(t *testing.T) { + cases := map[string]struct { + input s3.PutObjectInput + options s3.PresignOptions + expectPresignedURLHost string + expectRequestURIQuery []string + expectSignedHeader http.Header + expectMethod string + expectError string + }{ + "standard case": { + input: s3.PutObjectInput{ + Bucket: aws.String("mock-bucket"), + Key: aws.String("mockkey"), + Body: strings.NewReader("hello-world"), + }, + expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?", + expectRequestURIQuery: []string{ + "X-Amz-Expires=900", + "X-Amz-Credential", + "X-Amz-Date", + "x-id=PutObject", + "X-Amz-Signature", + }, + expectMethod: "PUT", + expectSignedHeader: http.Header{ + "content-length": []string{"11"}, + "content-type": []string{"application/octet-stream"}, + "host": []string{"mock-bucket.s3.us-west-2.amazonaws.com"}, + }, + }, + "seekable payload": { + input: s3.PutObjectInput{ + Bucket: aws.String("mock-bucket"), + Key: aws.String("mockkey"), + Body: bytes.NewReader([]byte("hello-world")), + }, + expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?", + expectRequestURIQuery: []string{ + "X-Amz-Expires=900", + "X-Amz-Credential", + "X-Amz-Date", + "x-id=PutObject", + "X-Amz-Signature", + }, + expectMethod: "PUT", + expectSignedHeader: http.Header{ + "content-length": []string{"11"}, + "content-type": []string{"application/octet-stream"}, + "host": []string{"mock-bucket.s3.us-west-2.amazonaws.com"}, + }, + }, + "unseekable payload": { + // unseekable payload succeeds as we disable content sha256 computation for streaming input + input: s3.PutObjectInput{ + Bucket: aws.String("mock-bucket"), + Key: aws.String("mockkey"), + Body: bytes.NewBuffer([]byte(`hello-world`)), + }, + expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?", + expectRequestURIQuery: []string{ + "X-Amz-Expires=900", + "X-Amz-Credential", + "X-Amz-Date", + "x-id=PutObject", + "X-Amz-Signature", + }, + expectMethod: "PUT", + expectSignedHeader: http.Header{ + "content-length": []string{"11"}, + "content-type": []string{"application/octet-stream"}, + "host": []string{"mock-bucket.s3.us-west-2.amazonaws.com"}, + }, + }, + "empty body": { + input: s3.PutObjectInput{ + Bucket: aws.String("mock-bucket"), + Key: aws.String("mockkey"), + Body: bytes.NewReader([]byte(``)), + }, + expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?", + expectRequestURIQuery: []string{ + "X-Amz-Expires=900", + "X-Amz-Credential", + "X-Amz-Date", + "x-id=PutObject", + "X-Amz-Signature", + }, + expectMethod: "PUT", + expectSignedHeader: http.Header{ + "content-type": {"application/octet-stream"}, + "host": []string{"mock-bucket.s3.us-west-2.amazonaws.com"}, + }, + }, + "nil body": { + input: s3.PutObjectInput{ + Bucket: aws.String("mock-bucket"), + Key: aws.String("mockkey"), + }, + expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?", + expectRequestURIQuery: []string{ + "X-Amz-Expires=900", + "X-Amz-Credential", + "X-Amz-Date", + "x-id=PutObject", + "X-Amz-Signature", + }, + expectMethod: "PUT", + expectSignedHeader: http.Header{ + "host": []string{"mock-bucket.s3.us-west-2.amazonaws.com"}, + }, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + cfg := aws.Config{ + Region: "us-west-2", + Credentials: unit.StubCredentialsProvider{}, + Retryer: aws.NoOpRetryer{}, + } + presignClient := s3.NewPresignClient(s3.NewFromConfig(cfg), func(options *s3.PresignOptions) { + options = &c.options + }) + + req, err := presignClient.PresignPutObject(ctx, &c.input) + if err != nil { + if len(c.expectError) == 0 { + t.Fatalf("expected no error, got %v", err) + } + // if expect error, match error and skip rest + if e, a := c.expectError, err.Error(); !strings.Contains(a, e) { + t.Fatalf("expected error to be %s, got %s", e, a) + } + } else { + if len(c.expectError) != 0 { + t.Fatalf("expected error to be %v, got none", c.expectError) + } + } + + if e, a := c.expectPresignedURLHost, req.URL; !strings.Contains(a, e) { + t.Fatalf("expected presigned url to contain host %s, got %s", e, a) + } + + if len(c.expectRequestURIQuery) != 0 { + for _, label := range c.expectRequestURIQuery { + if e, a := label, req.URL; !strings.Contains(a, e) { + t.Fatalf("expected presigned url to contain %v label in url: %v", label, req.URL) + } + } + } + + if e, a := c.expectSignedHeader, req.SignedHeader; len(cmp.Diff(e, a)) != 0 { + t.Fatalf("expected signed header to be %s, got %s, \n diff : %s", e, a, cmp.Diff(e, a)) + } + + if e, a := c.expectMethod, req.Method; !strings.EqualFold(e, a) { + t.Fatalf("expected presigning Method to be %s, got %s", e, a) + } + + }) + } +} diff --git a/service/s3/internal/customizations/presigned_expires.go b/service/s3/internal/customizations/presigned_expires.go new file mode 100644 index 00000000000..2ea6318d4a9 --- /dev/null +++ b/service/s3/internal/customizations/presigned_expires.go @@ -0,0 +1,49 @@ +package customizations + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/awslabs/smithy-go/middleware" + smithyhttp "github.com/awslabs/smithy-go/transport/http" +) + +// AddExpiresOnPresignedURL represents a build middleware used to assign +// expiration on a presigned URL. +type AddExpiresOnPresignedURL struct { + + // Expires is time.Duration within which presigned url should be expired. + // This should be the duration in seconds the presigned URL should be considered valid for. + // By default the S3 presigned url expires in 15 minutes ie. 900 seconds. + Expires time.Duration +} + +// ID representing the middleware +func (*AddExpiresOnPresignedURL) ID() string { + return "S3:AddExpiresOnPresignedURL" +} + +// HandleBuild handles the build step middleware behavior +func (m *AddExpiresOnPresignedURL) HandleBuild(ctx context.Context, in middleware.BuildInput, next middleware.BuildHandler) ( + out middleware.BuildOutput, metadata middleware.Metadata, err error, +) { + // if expiration is unset skip this middleware + if m.Expires == 0 { + // default to 15 * time.Minutes + m.Expires = 15 * time.Minute + } + + req, ok := in.Request.(*smithyhttp.Request) + if !ok { + return out, metadata, fmt.Errorf("unknown transport type %T", req) + } + + // set S3 X-AMZ-Expires header + query := req.URL.Query() + query.Set("X-Amz-Expires", strconv.FormatInt(int64(m.Expires/time.Second), 10)) + req.URL.RawQuery = query.Encode() + + return next.HandleBuild(ctx, in) +}