Added the clients directory

This commit is contained in:
evdeg 2020-08-05 17:53:39 -07:00
parent 943d6e5d58
commit 2f93f06357
89 changed files with 8830 additions and 1 deletions

View File

@ -8,4 +8,3 @@ See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more inform
## License
This project is licensed under the Apache-2.0 License.

View File

@ -0,0 +1,10 @@
#Ignore bin directories
bin
**/bin
#Ignore obj directories
obj
**/obj
#ignore .vs directories
.vs
#misc
.DS_Store

View File

@ -0,0 +1,96 @@
# Selling Partner API Authentication/Authorization Library
This library provides helper classes for use when signing HTTP requests for Amazon Selling Partner APIs. It is intended for use
with the Selling Partner API Client Libraries generated by [swagger codegen](https://swagger.io/tools/swagger-codegen/)
using the RestSharp library. It can also be integrated into custom projects.
## LWAAuthorizationSigner
Obtains and signs a request with an access token from LWA (Login with Amazon) for the specified endpoint using the provided LWA credentials.
*Example*
```
using RestSharp;
using Amazon.SellingPartnerAPIAA;
string resource = "/my/api/path";
RestClient restClient = new RestClient("https://...");
IRestRequest restRequest = new RestRequest(resource, Method.GET);
// Seller APIs
LWAAuthorizationCredentials lwaAuthorizationCredentials = new LWAAuthorizationCredentials
{
ClientId = "...",
ClientSecret = "",
RefreshToken = "",
Endpoint = new Uri("...")
};
/* Sellerless APIs
The Selling Partner API scopes can be retrieved from the ScopeConstants class and used to specify a list of scopes of an LWAAuthorizationCredentials instance. */
LWAAuthorizationCredentials lwaAuthorizationCredentials = new LWAAuthorizationCredentials
{
ClientId = "...",
ClientSecret = "",
Scopes = new List<string>() { ScopeConstants.ScopeNotificationsAPI, ScopeConstants.ScopeMigrationAPI }
Endpoint = new Uri("...")
};
restRequest = new LWAAuthorizationSigner(lwaAuthorizationCredentials).Sign(restRequest);
```
Note the IRestRequest reference is treated as **mutable** when signed.
## AWSSigV4Signer
Signs a request with [AWS Signature Version 4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html)
using the provided AWS developer account credentials.
*Example*
```
using RestSharp;
using Amazon.SellingPartnerAPIAA;
string resource = "/my/api/path";
RestClient restClient = new RestClient("https://...");
IRestRequest restRequest = new RestRequest(resource, Method.GET);
AWSAuthenticationCredentials awsAuthenticationCredentials = new AWSAuthenticationCredentials
{
AccessKeyId = "..."
SecretKey = "..."
Region = "..."
};
restRequest = new AWSSigV4Signer(awsAuthenticationCredentials)
.Sign(restRequest, restClient.BaseUrl.Host);
```
Note the IRestRequest reference is treated as **mutable** when signed.
## Resources
This package features Mustache templates designed for use with [swagger codegen](https://swagger.io/tools/swagger-codegen/).
When you build Selling Partner API Swagger models with these templates, they help generate a rich SDK with functionality to invoke Selling Partner APIs built in. The templates are located in *resources/swagger-codegen*.
## Building
This package is built as a .NET Standard Library via a Visual Studio Solution with implementation and test projects. The Visual Studio Community Edition can be obtained for free from Microsoft and used to build the solution and generate a .dll assembly which can be imported into other projects.
## Dependencies
All dependencies can be installed via NuGet
- RestSharp - 105.1.0
- Newtonsoft.Json 12.0.3
- NETStandard.Library 2.0.3 (platform-specific implementation requirements are documented on the [Microsoft .NET Guide](https://docs.microsoft.com/en-us/dotnet/standard/net-standard))
## License
Swagger Codegen templates are subject to the [Swagger Codegen License](https://github.com/swagger-api/swagger-codegen#license).
All other work licensed as follows:
Copyright 2020 Amazon.com, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this library except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,23 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.SellingPartnerAPIAA", "src\Amazon.SellingPartnerAPIAA\Amazon.SellingPartnerAPIAA.csproj", "{64339397-3AAB-49D3-8B50-7A467B16D545}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.SellingPartnerAPIAATests", "test\Amazon.SellingPartnerAPIAATests\Amazon.SellingPartnerAPIAATests.csproj", "{12B130EB-1087-4F88-BDFA-3088868C0A46}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{64339397-3AAB-49D3-8B50-7A467B16D545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{64339397-3AAB-49D3-8B50-7A467B16D545}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64339397-3AAB-49D3-8B50-7A467B16D545}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64339397-3AAB-49D3-8B50-7A467B16D545}.Release|Any CPU.Build.0 = Release|Any CPU
{12B130EB-1087-4F88-BDFA-3088868C0A46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{12B130EB-1087-4F88-BDFA-3088868C0A46}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12B130EB-1087-4F88-BDFA-3088868C0A46}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12B130EB-1087-4F88-BDFA-3088868C0A46}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,20 @@
namespace Amazon.SellingPartnerAPIAA
{
public class AWSAuthenticationCredentials
{
/**
* AWS IAM User Access Key Id
*/
public string AccessKeyId { get; set; }
/**
* AWS IAM User Secret Key
*/
public string SecretKey { get; set; }
/**
* AWS Region
*/
public string Region { get; set; }
}
}

View File

@ -0,0 +1,82 @@
using System;
using System.Text;
using RestSharp;
namespace Amazon.SellingPartnerAPIAA
{
public class AWSSigV4Signer
{
public virtual AWSSignerHelper AwsSignerHelper { get; set; }
private AWSAuthenticationCredentials awsCredentials;
/// <summary>
/// Constructor for AWSSigV4Signer
/// </summary>
/// <param name="awsAuthenticationCredentials">AWS Developer Account Credentials</param>
public AWSSigV4Signer(AWSAuthenticationCredentials awsAuthenticationCredentials)
{
awsCredentials = awsAuthenticationCredentials;
AwsSignerHelper = new AWSSignerHelper();
}
/// <summary>
/// Signs a Request with AWS Signature Version 4
/// </summary>
/// <param name="request">RestRequest which needs to be signed</param>
/// <param name="host">Request endpoint</param>
/// <returns>RestRequest with AWS Signature</returns>
public IRestRequest Sign(IRestRequest request, string host)
{
DateTime signingDate = AwsSignerHelper.InitializeHeaders(request, host);
string signedHeaders = AwsSignerHelper.ExtractSignedHeaders(request);
string hashedCanonicalRequest = CreateCanonicalRequest(request, signedHeaders);
string stringToSign = AwsSignerHelper.BuildStringToSign(signingDate,
hashedCanonicalRequest,
awsCredentials.Region);
string signature = AwsSignerHelper.CalculateSignature(stringToSign,
signingDate,
awsCredentials.SecretKey,
awsCredentials.Region);
AwsSignerHelper.AddSignature(request,
awsCredentials.AccessKeyId,
signedHeaders,
signature,
awsCredentials.Region,
signingDate);
return request;
}
private string CreateCanonicalRequest(IRestRequest restRequest, string signedHeaders)
{
var canonicalizedRequest = new StringBuilder();
//Request Method
canonicalizedRequest.AppendFormat("{0}\n", restRequest.Method);
//CanonicalURI
canonicalizedRequest.AppendFormat("{0}\n", AwsSignerHelper.ExtractCanonicalURIParameters(restRequest.Resource));
//CanonicalQueryString
canonicalizedRequest.AppendFormat("{0}\n", AwsSignerHelper.ExtractCanonicalQueryString(restRequest));
//CanonicalHeaders
canonicalizedRequest.AppendFormat("{0}\n", AwsSignerHelper.ExtractCanonicalHeaders(restRequest));
//SignedHeaders
canonicalizedRequest.AppendFormat("{0}\n", signedHeaders);
// Hash(digest) the payload in the body
canonicalizedRequest.AppendFormat(AwsSignerHelper.HashRequestBody(restRequest));
string canonicalRequest = canonicalizedRequest.ToString();
//Create a digest(hash) of the canonical request
return Utils.ToHex(Utils.Hash(canonicalRequest));
}
}
}

View File

@ -0,0 +1,251 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using RestSharp;
using System.Text.RegularExpressions;
using System.Globalization;
namespace Amazon.SellingPartnerAPIAA
{
public class AWSSignerHelper
{
public const string ISO8601BasicDateTimeFormat = "yyyyMMddTHHmmssZ";
public const string ISO8601BasicDateFormat = "yyyyMMdd";
public const string XAmzDateHeaderName = "X-Amz-Date";
public const string AuthorizationHeaderName = "Authorization";
public const string CredentialSubHeaderName = "Credential";
public const string SignatureSubHeaderName = "Signature";
public const string SignedHeadersSubHeaderName = "SignedHeaders";
public const string HostHeaderName = "host";
public const string Scheme = "AWS4";
public const string Algorithm = "HMAC-SHA256";
public const string TerminationString = "aws4_request";
public const string ServiceName = "execute-api";
public const string Slash = "/";
private readonly static Regex CompressWhitespaceRegex = new Regex("\\s+");
public virtual IDateHelper DateHelper { get; set; }
public AWSSignerHelper()
{
DateHelper = new SigningDateHelper();
}
/// <summary>
/// Returns URI encoded version of absolute path
/// </summary>
/// <param name="resource">Resource path(absolute path) from the request</param>
/// <returns>URI encoded version of absolute path</returns>
public virtual string ExtractCanonicalURIParameters(string resource)
{
string canonicalUri = string.Empty;
if (string.IsNullOrEmpty(resource))
{
canonicalUri = Slash;
}
else
{
if (!resource.StartsWith(Slash))
{
canonicalUri = Slash;
}
//Split path at / into segments
IEnumerable<string> encodedSegments = resource.Split(new char[] { '/' }, StringSplitOptions.None);
// Encode twice
encodedSegments = encodedSegments.Select(segment => Utils.UrlEncode(segment));
encodedSegments = encodedSegments.Select(segment => Utils.UrlEncode(segment));
canonicalUri += string.Join(Slash, encodedSegments.ToArray());
}
return canonicalUri;
}
/// <summary>
/// Returns query parameters in canonical order with URL encoding
/// </summary>
/// <param name="request">RestRequest</param>
/// <returns>Query parameters in canonical order with URL encoding</returns>
public virtual string ExtractCanonicalQueryString(IRestRequest request)
{
IDictionary<string, string> queryParameters = request.Parameters
.Where(parameter => ParameterType.QueryString.Equals(parameter.Type))
.ToDictionary(header => header.Name.Trim().ToString(), header => header.Value.ToString());
SortedDictionary<string, string> sortedqueryParameters = new SortedDictionary<string, string>(queryParameters);
StringBuilder canonicalQueryString = new StringBuilder();
foreach (var key in sortedqueryParameters.Keys)
{
if (canonicalQueryString.Length > 0)
{
canonicalQueryString.Append("&");
}
canonicalQueryString.AppendFormat("{0}={1}",
Utils.UrlEncode(key),
Utils.UrlEncode(sortedqueryParameters[key]));
}
return canonicalQueryString.ToString();
}
/// <summary>
/// Returns Http headers in canonical order with all header names to lowercase
/// </summary>
/// <param name="request">RestRequest</param>
/// <returns>Returns Http headers in canonical order</returns>
public virtual string ExtractCanonicalHeaders(IRestRequest request)
{
IDictionary<string, string> headers = request.Parameters
.Where(parameter => ParameterType.HttpHeader.Equals(parameter.Type))
.ToDictionary(header => header.Name.Trim().ToLowerInvariant(), header => header.Value.ToString());
SortedDictionary<string, string> sortedHeaders = new SortedDictionary<string, string>(headers);
StringBuilder headerString = new StringBuilder();
foreach (string headerName in sortedHeaders.Keys)
{
headerString.AppendFormat("{0}:{1}\n",
headerName,
CompressWhitespaceRegex.Replace(sortedHeaders[headerName].Trim(), " "));
}
return headerString.ToString();
}
/// <summary>
/// Returns list(as string) of Http headers in canonical order
/// </summary>
/// <param name="request">RestRequest</param>
/// <returns>List of Http headers in canonical order</returns>
public virtual string ExtractSignedHeaders(IRestRequest request)
{
List<string> rawHeaders = request.Parameters.Where(parameter => ParameterType.HttpHeader.Equals(parameter.Type))
.Select(header => header.Name.Trim().ToLowerInvariant())
.ToList();
rawHeaders.Sort(StringComparer.OrdinalIgnoreCase);
return string.Join(";", rawHeaders);
}
/// <summary>
/// Returns hexadecimal hashed value(using SHA256) of payload in the body of request
/// </summary>
/// <param name="request">RestRequest</param>
/// <returns>Hexadecimal hashed value of payload in the body of request</returns>
public virtual string HashRequestBody(IRestRequest request)
{
Parameter body = request.Parameters.FirstOrDefault(parameter => ParameterType.RequestBody.Equals(parameter.Type));
string value = body != null ? body.Value.ToString() : string.Empty;
return Utils.ToHex(Utils.Hash(value));
}
/// <summary>
/// Builds the string for signing using signing date, hashed canonical request and region
/// </summary>
/// <param name="signingDate">Signing Date</param>
/// <param name="hashedCanonicalRequest">Hashed Canonical Request</param>
/// <param name="region">Region</param>
/// <returns>String to be used for signing</returns>
public virtual string BuildStringToSign(DateTime signingDate, string hashedCanonicalRequest, string region)
{
string scope = BuildScope(signingDate, region);
string stringToSign = string.Format(CultureInfo.InvariantCulture, "{0}-{1}\n{2}\n{3}\n{4}",
Scheme,
Algorithm,
signingDate.ToString(ISO8601BasicDateTimeFormat, CultureInfo.InvariantCulture),
scope,
hashedCanonicalRequest);
return stringToSign;
}
/// <summary>
/// Sets AWS4 mandated 'x-amz-date' header, returning the date/time that will
/// be used throughout the signing process.
/// </summary>
/// <param name="restRequest">RestRequest</param>
/// <param name="host">Request endpoint</param>
/// <returns>Date and time used for x-amz-date, in UTC</returns>
public virtual DateTime InitializeHeaders(IRestRequest restRequest, string host)
{
restRequest.Parameters.RemoveAll(parameter => ParameterType.HttpHeader.Equals(parameter.Type)
&& parameter.Name == XAmzDateHeaderName);
restRequest.Parameters.RemoveAll(parameter => ParameterType.HttpHeader.Equals(parameter.Type)
&& parameter.Name == HostHeaderName);
DateTime signingDate = DateHelper.GetUtcNow();
restRequest.AddHeader(XAmzDateHeaderName, signingDate.ToString(ISO8601BasicDateTimeFormat, CultureInfo.InvariantCulture));
restRequest.AddHeader(HostHeaderName, host);
return signingDate;
}
/// <summary>
/// Calculates AWS4 signature for the string, prepared for signing
/// </summary>
/// <param name="stringToSign">String to be signed</param>
/// <param name="signingDate">Signing Date</param>
/// <param name="secretKey">Secret Key</param>
/// <param name="region">Region</param>
/// <returns>AWS4 Signature</returns>
public virtual string CalculateSignature(string stringToSign,
DateTime signingDate,
string secretKey,
string region)
{
string date = signingDate.ToString(ISO8601BasicDateFormat, CultureInfo.InvariantCulture);
byte[] kSecret = Encoding.UTF8.GetBytes(Scheme + secretKey);
byte[] kDate = Utils.GetKeyedHash(kSecret, date);
byte[] kRegion = Utils.GetKeyedHash(kDate, region);
byte[] kService = Utils.GetKeyedHash(kRegion, ServiceName);
byte[] kSigning = Utils.GetKeyedHash(kService, TerminationString);
// Calculate the signature
return Utils.ToHex(Utils.GetKeyedHash(kSigning, stringToSign));
}
/// <summary>
/// Add a signature to a request in the form of an 'Authorization' header
/// </summary>
/// <param name="restRequest">Request to be signed</param>
/// <param name="accessKeyId">Access Key Id</param>
/// <param name="signedHeaders">Signed Headers</param>
/// <param name="signature">The signature to add</param>
/// <param name="region">AWS region for the request</param>
/// <param name="signingDate">Signature date</param>
public virtual void AddSignature(IRestRequest restRequest,
string accessKeyId,
string signedHeaders,
string signature,
string region,
DateTime signingDate)
{
string scope = BuildScope(signingDate, region);
StringBuilder authorizationHeaderValueBuilder = new StringBuilder();
authorizationHeaderValueBuilder.AppendFormat("{0}-{1}", Scheme, Algorithm);
authorizationHeaderValueBuilder.AppendFormat(" {0}={1}/{2},", CredentialSubHeaderName, accessKeyId, scope);
authorizationHeaderValueBuilder.AppendFormat(" {0}={1},", SignedHeadersSubHeaderName, signedHeaders);
authorizationHeaderValueBuilder.AppendFormat(" {0}={1}", SignatureSubHeaderName, signature);
restRequest.AddHeader(AuthorizationHeaderName, authorizationHeaderValueBuilder.ToString());
}
private static string BuildScope(DateTime signingDate, string region)
{
return string.Format("{0}/{1}/{2}/{3}",
signingDate.ToString(ISO8601BasicDateFormat, CultureInfo.InvariantCulture),
region,
ServiceName,
TerminationString);
}
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RestSharp" Version="105.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<ItemGroup>
<Folder Include="resources\" />
<Folder Include="resources\swagger-codegen\" />
<Folder Include="resources\swagger-codegen\templates\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
using System;
namespace Amazon.SellingPartnerAPIAA
{
public interface IDateHelper
{
DateTime GetUtcNow();
}
}

View File

@ -0,0 +1,35 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Amazon.SellingPartnerAPIAA
{
public class LWAAccessTokenRequestMeta
{
[JsonProperty(PropertyName = "grant_type")]
public string GrantType { get; set; }
[JsonProperty(PropertyName = "refresh_token")]
public string RefreshToken { get; set; }
[JsonProperty(PropertyName = "client_id")]
public string ClientId { get; set; }
[JsonProperty(PropertyName = "client_secret")]
public string ClientSecret { get; set; }
[JsonProperty(PropertyName = "scope")]
public string Scope { get; set; }
public override bool Equals(object obj)
{
LWAAccessTokenRequestMeta other = obj as LWAAccessTokenRequestMeta;
return other != null &&
this.GrantType == other.GrantType &&
this.RefreshToken == other.RefreshToken &&
this.ClientId == other.ClientId &&
this.ClientSecret == other.ClientSecret &&
this.Scope == other.Scope;
}
}
}

View File

@ -0,0 +1,40 @@
using System.Linq;
namespace Amazon.SellingPartnerAPIAA
{
public class LWAAccessTokenRequestMetaBuilder
{
public const string SellerAPIGrantType = "refresh_token";
public const string SellerlessAPIGrantType = "client_credentials";
private const string Delimiter = " ";
/// <summary>
/// Builds an instance of LWAAccessTokenRequestMeta modeling appropriate LWA token
/// request params based on configured LWAAuthorizationCredentials
/// </summary>
/// <param name="lwaAuthorizationCredentials">LWA Authorization Credentials</param>
/// <returns></returns>
public virtual LWAAccessTokenRequestMeta Build(LWAAuthorizationCredentials lwaAuthorizationCredentials)
{
LWAAccessTokenRequestMeta lwaAccessTokenRequestMeta = new LWAAccessTokenRequestMeta()
{
ClientId = lwaAuthorizationCredentials.ClientId,
ClientSecret = lwaAuthorizationCredentials.ClientSecret,
RefreshToken = lwaAuthorizationCredentials.RefreshToken
};
if (lwaAuthorizationCredentials.Scopes == null || lwaAuthorizationCredentials.Scopes.Count == 0)
{
lwaAccessTokenRequestMeta.GrantType = SellerAPIGrantType;
}
else
{
lwaAccessTokenRequestMeta.Scope = string.Join(Delimiter, lwaAuthorizationCredentials.Scopes);
lwaAccessTokenRequestMeta.GrantType = SellerlessAPIGrantType;
}
return lwaAccessTokenRequestMeta;
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
namespace Amazon.SellingPartnerAPIAA
{
public class LWAAuthorizationCredentials
{
public LWAAuthorizationCredentials()
{
this.Scopes = new List<string>();
}
/**
* LWA Client Id
*/
public string ClientId { get; set; }
/**
* LWA Client Secret
*/
public string ClientSecret { get; set; }
/**
* LWA Refresh Token
*/
public string RefreshToken { get; set; }
/**
* LWA Authorization Server Endpoint
*/
public Uri Endpoint { get; set; }
/**
* LWA Authorization Scopes
*/
public List<string> Scopes { get; set; }
}
}

View File

@ -0,0 +1,34 @@
using RestSharp;
namespace Amazon.SellingPartnerAPIAA
{
public class LWAAuthorizationSigner
{
public const string AccessTokenHeaderName = "x-amz-access-token";
public LWAClient LWAClient { get; set; }
/// <summary>
/// Constructor for LWAAuthorizationSigner
/// </summary>
/// <param name="lwaAuthorizationCredentials">LWA Authorization Credentials for token exchange</param>
public LWAAuthorizationSigner(LWAAuthorizationCredentials lwaAuthorizationCredentials)
{
LWAClient = new LWAClient(lwaAuthorizationCredentials);
}
/// <summary>
/// Signs a request with LWA Access Token
/// </summary>
/// <param name="restRequest">Request to sign</param>
/// <returns>restRequest with LWA signature</returns>
public IRestRequest Sign(IRestRequest restRequest)
{
string accessToken = LWAClient.GetAccessToken();
restRequest.AddHeader(AccessTokenHeaderName, accessToken);
return restRequest;
}
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RestSharp;
namespace Amazon.SellingPartnerAPIAA
{
public class LWAClient
{
public const string AccessTokenKey = "access_token";
public const string JsonMediaType = "application/json; charset=utf-8";
public IRestClient RestClient { get; set; }
public LWAAccessTokenRequestMetaBuilder LWAAccessTokenRequestMetaBuilder { get; set; }
public LWAAuthorizationCredentials LWAAuthorizationCredentials { get; private set; }
public LWAClient(LWAAuthorizationCredentials lwaAuthorizationCredentials)
{
LWAAuthorizationCredentials = lwaAuthorizationCredentials;
LWAAccessTokenRequestMetaBuilder = new LWAAccessTokenRequestMetaBuilder();
RestClient = new RestClient(LWAAuthorizationCredentials.Endpoint.GetLeftPart(System.UriPartial.Authority));
}
/// <summary>
/// Retrieves access token from LWA
/// </summary>
/// <param name="lwaAccessTokenRequestMeta">LWA AccessTokenRequest metadata</param>
/// <returns>LWA Access Token</returns>
public virtual string GetAccessToken()
{
LWAAccessTokenRequestMeta lwaAccessTokenRequestMeta = LWAAccessTokenRequestMetaBuilder.Build(LWAAuthorizationCredentials);
var accessTokenRequest = new RestRequest(LWAAuthorizationCredentials.Endpoint.AbsolutePath, Method.POST);
string jsonRequestBody = JsonConvert.SerializeObject(lwaAccessTokenRequestMeta);
accessTokenRequest.AddParameter(JsonMediaType, jsonRequestBody, ParameterType.RequestBody);
string accessToken;
try
{
var response = RestClient.Execute(accessTokenRequest);
if (!IsSuccessful(response))
{
throw new IOException("Unsuccessful LWA token exchange", response.ErrorException);
}
JObject responseJson = JObject.Parse(response.Content);
accessToken = responseJson.GetValue(AccessTokenKey).ToString();
}
catch (Exception e)
{
throw new SystemException("Error getting LWA Access Token", e);
}
return accessToken;
}
private bool IsSuccessful(IRestResponse response)
{
int statusCode = (int)response.StatusCode;
return statusCode >= 200 && statusCode <= 299 && response.ResponseStatus == ResponseStatus.Completed;
}
}
}

View File

@ -0,0 +1,8 @@
namespace Amazon.SellingPartnerAPIAA
{
public class ScopeConstants
{
public const string ScopeNotificationsAPI = "sellingpartnerapi::notifications";
public const string ScopeMigrationAPI = "sellingpartnerapi::migration";
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace Amazon.SellingPartnerAPIAA
{
public class SigningDateHelper : IDateHelper
{
public DateTime GetUtcNow()
{
return DateTime.UtcNow;
}
}
}

View File

@ -0,0 +1,73 @@
using System.Text;
using System.Security.Cryptography;
using System.Globalization;
namespace Amazon.SellingPartnerAPIAA
{
public static class Utils
{
public const string ValidUrlCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
/// <summary>
/// Returns URL encoded version of input data according to RFC-3986
/// </summary>
/// <param name="data">String to be URL-encoded</param>
/// <returns>URL encoded version of input data</returns>
public static string UrlEncode(string data)
{
StringBuilder encoded = new StringBuilder();
foreach (char symbol in Encoding.UTF8.GetBytes(data))
{
if (ValidUrlCharacters.IndexOf(symbol) != -1)
{
encoded.Append(symbol);
}
else
{
encoded.Append("%").Append(string.Format(CultureInfo.InvariantCulture, "{0:X2}", (int)symbol));
}
}
return encoded.ToString();
}
/// <summary>
/// Returns hashed value of input data using SHA256
/// </summary>
/// <param name="data">String to be hashed</param>
/// <returns>Hashed value of input data</returns>
public static byte[] Hash(string data)
{
return new SHA256CryptoServiceProvider().ComputeHash(Encoding.UTF8.GetBytes(data));
}
/// <summary>
/// Returns lowercase hexadecimal string of input byte array
/// </summary>
/// <param name="data">Data to be converted</param>
/// <returns>Lowercase hexadecimal string</returns>
public static string ToHex(byte[] data)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < data.Length; i++)
{
sb.Append(data[i].ToString("x2", CultureInfo.InvariantCulture));
}
return sb.ToString();
}
/// <summary>
/// Computes the hash of given string using the specified key with HMACSHA256
/// </summary>
/// <param name="key">Key</param>
/// <param name="value">String to be hashed</param>
/// <returns>Hashed value of input data</returns>
public static byte[] GetKeyedHash(byte[] key, string value)
{
KeyedHashAlgorithm hashAlgorithm = new HMACSHA256(key);
hashAlgorithm.Initialize();
return hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(value));
}
}
}

View File

@ -0,0 +1,576 @@
{{>partial_header}}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using System.IO;
{{^netStandard}}
{{^supportsUWP}}
using System.Web;
{{/supportsUWP}}
{{/netStandard}}
using System.Linq;
using System.Net;
using System.Text;
using Newtonsoft.Json;
{{#netStandard}}
using RestSharp.Portable;
using RestSharp.Portable.HttpClient;
{{/netStandard}}
{{^netStandard}}
using RestSharp;
{{/netStandard}}
using Amazon.SellingPartnerAPIAA;
namespace {{packageName}}.Client
{
/// <summary>
/// API client is mainly responsible for making the HTTP call to the API backend.
/// </summary>
{{>visibility}} partial class ApiClient
{
private LWAAuthorizationSigner lwaAuthorizationSigner;
private AWSSigV4Signer awsSigV4Signer;
private JsonSerializerSettings serializerSettings = new JsonSerializerSettings
{
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
};
/// <summary>
/// Allows for extending request processing for <see cref="ApiClient"/> generated code.
/// </summary>
/// <param name="request">The RestSharp request object</param>
private void InterceptRequest(IRestRequest request)
{
lwaAuthorizationSigner.Sign(request);
awsSigV4Signer.Sign(request, RestClient.BaseUrl.Host);
}
/// <summary>
/// Allows for extending response processing for <see cref="ApiClient"/> generated code.
/// </summary>
/// <param name="request">The RestSharp request object</param>
/// <param name="response">The RestSharp response object</param>
partial void InterceptResponse(IRestRequest request, IRestResponse response);
/// <summary>
/// Initializes a new instance of the <see cref="ApiClient" /> class
/// with default base path ({{{basePath}}}).
/// </summary>
/// <param name="config">An instance of Configuration.</param>
public ApiClient(Configuration config)
{
Configuration = config ?? {{packageName}}.Client.Configuration.Default;
RestClient = new RestClient(Configuration.BasePath);
{{#netStandard}}
RestClient.IgnoreResponseStatusCode = true;
{{/netStandard}}
lwaAuthorizationSigner = new LWAAuthorizationSigner(Configuration.AuthorizationCredentials);
awsSigV4Signer = new AWSSigV4Signer(Configuration.AuthenticationCredentials);
}
/// <summary>
/// Gets or sets the default API client for making HTTP calls.
/// </summary>
/// <value>The default API client.</value>
[Obsolete("ApiClient.Default is deprecated, please use 'Configuration.Default.ApiClient' instead.")]
public static ApiClient Default;
/// <summary>
/// Gets or sets an instance of the IReadableConfiguration.
/// </summary>
/// <value>An instance of the IReadableConfiguration.</value>
/// <remarks>
/// <see cref="IReadableConfiguration"/> helps us to avoid modifying possibly global
/// configuration values from within a given client. It does not guarantee thread-safety
/// of the <see cref="Configuration"/> instance in any way.
/// </remarks>
public IReadableConfiguration Configuration { get; set; }
/// <summary>
/// Gets or sets the RestClient.
/// </summary>
/// <value>An instance of the RestClient</value>
public RestClient RestClient { get; set; }
// Creates and sets up a RestRequest prior to a call.
private RestRequest PrepareRequest(
String path, {{^netStandard}}RestSharp.{{/netStandard}}Method method, List<KeyValuePair<String, String>> queryParams, Object postBody,
Dictionary<String, String> headerParams, Dictionary<String, String> formParams,
Dictionary<String, FileParameter> fileParams, Dictionary<String, String> pathParams,
String contentType)
{
var request = new RestRequest(path, method);
{{#netStandard}}
// disable ResetSharp.Portable built-in serialization
request.Serializer = null;
{{/netStandard}}
// add path parameter, if any
foreach(var param in pathParams)
request.AddParameter(param.Key, param.Value, ParameterType.UrlSegment);
// add header parameter, if any
foreach(var param in headerParams)
request.AddHeader(param.Key, param.Value);
// add query parameter, if any
foreach(var param in queryParams)
request.AddQueryParameter(param.Key, param.Value);
// add form parameter, if any
foreach(var param in formParams)
request.AddParameter(param.Key, param.Value);
// add file parameter, if any
foreach(var param in fileParams)
{
{{#netStandard}}
request.AddFile(param.Value);
{{/netStandard}}
{{^netStandard}}
{{^supportsUWP}}
request.AddFile(param.Value.Name, param.Value.Writer, param.Value.FileName, param.Value.ContentType);
{{/supportsUWP}}
{{#supportsUWP}}
byte[] paramWriter = null;
param.Value.Writer = delegate (Stream stream) { paramWriter = ToByteArray(stream); };
request.AddFile(param.Value.Name, paramWriter, param.Value.FileName, param.Value.ContentType);
{{/supportsUWP}}
{{/netStandard}}
}
if (postBody != null) // http body (model or byte[]) parameter
{
{{#netStandard}}
request.AddParameter(new Parameter { Value = postBody, Type = ParameterType.RequestBody, ContentType = contentType });
{{/netStandard}}
{{^netStandard}}
request.AddParameter(contentType, postBody, ParameterType.RequestBody);
{{/netStandard}}
}
return request;
}
/// <summary>
/// Makes the HTTP request (Sync).
/// </summary>
/// <param name="path">URL path.</param>
/// <param name="method">HTTP method.</param>
/// <param name="queryParams">Query parameters.</param>
/// <param name="postBody">HTTP body (POST request).</param>
/// <param name="headerParams">Header parameters.</param>
/// <param name="formParams">Form parameters.</param>
/// <param name="fileParams">File parameters.</param>
/// <param name="pathParams">Path parameters.</param>
/// <param name="contentType">Content Type of the request</param>
/// <returns>Object</returns>
public Object CallApi(
String path, {{^netStandard}}RestSharp.{{/netStandard}}Method method, List<KeyValuePair<String, String>> queryParams, Object postBody,
Dictionary<String, String> headerParams, Dictionary<String, String> formParams,
Dictionary<String, FileParameter> fileParams, Dictionary<String, String> pathParams,
String contentType)
{
var request = PrepareRequest(
path, method, queryParams, postBody, headerParams, formParams, fileParams,
pathParams, contentType);
// set timeout
{{#netStandard}}RestClient.Timeout = TimeSpan.FromMilliseconds(Configuration.Timeout);{{/netStandard}}
{{^netStandard}}RestClient.Timeout = Configuration.Timeout;{{/netStandard}}
// set user agent
RestClient.UserAgent = Configuration.UserAgent;
InterceptRequest(request);
{{#netStandard}}
var response = RestClient.Execute(request).Result;
{{/netStandard}}
{{^netStandard}}
{{^supportsUWP}}
var response = RestClient.Execute(request);
{{/supportsUWP}}
{{#supportsUWP}}
// Using async method to perform sync call (uwp-only)
var response = RestClient.ExecuteTaskAsync(request).Result;
{{/supportsUWP}}
{{/netStandard}}
InterceptResponse(request, response);
return (Object) response;
}
{{#supportsAsync}}
/// <summary>
/// Makes the asynchronous HTTP request.
/// </summary>
/// <param name="path">URL path.</param>
/// <param name="method">HTTP method.</param>
/// <param name="queryParams">Query parameters.</param>
/// <param name="postBody">HTTP body (POST request).</param>
/// <param name="headerParams">Header parameters.</param>
/// <param name="formParams">Form parameters.</param>
/// <param name="fileParams">File parameters.</param>
/// <param name="pathParams">Path parameters.</param>
/// <param name="contentType">Content type.</param>
/// <returns>The Task instance.</returns>
public async System.Threading.Tasks.Task<Object> CallApiAsync(
String path, {{^netStandard}}RestSharp.{{/netStandard}}Method method, List<KeyValuePair<String, String>> queryParams, Object postBody,
Dictionary<String, String> headerParams, Dictionary<String, String> formParams,
Dictionary<String, FileParameter> fileParams, Dictionary<String, String> pathParams,
String contentType)
{
var request = PrepareRequest(
path, method, queryParams, postBody, headerParams, formParams, fileParams,
pathParams, contentType);
InterceptRequest(request);
var response = await RestClient.Execute{{^netStandard}}TaskAsync{{/netStandard}}(request);
InterceptResponse(request, response);
return (Object)response;
}{{/supportsAsync}}
/// <summary>
/// Escape string (url-encoded).
/// </summary>
/// <param name="str">String to be escaped.</param>
/// <returns>Escaped string.</returns>
public string EscapeString(string str)
{
return UrlEncode(str);
}
/// <summary>
/// Create FileParameter based on Stream.
/// </summary>
/// <param name="name">Parameter name.</param>
/// <param name="stream">Input stream.</param>
/// <returns>FileParameter.</returns>
public FileParameter ParameterToFile(string name, Stream stream)
{
if (stream is FileStream)
return FileParameter.Create(name, ReadAsBytes(stream), Path.GetFileName(((FileStream)stream).Name));
else
return FileParameter.Create(name, ReadAsBytes(stream), "no_file_name_provided");
}
/// <summary>
/// If parameter is DateTime, output in a formatted string (default ISO 8601), customizable with Configuration.DateTime.
/// If parameter is a list, join the list with ",".
/// Otherwise just return the string.
/// </summary>
/// <param name="obj">The parameter (header, path, query, form).</param>
/// <returns>Formatted string.</returns>
public string ParameterToString(object obj)
{
if (obj is DateTime)
// Return a formatted date string - Can be customized with Configuration.DateTimeFormat
// Defaults to an ISO 8601, using the known as a Round-trip date/time pattern ("o")
// https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx#Anchor_8
// For example: 2009-06-15T13:45:30.0000000
return ((DateTime)obj).ToString (Configuration.DateTimeFormat);
else if (obj is DateTimeOffset)
// Return a formatted date string - Can be customized with Configuration.DateTimeFormat
// Defaults to an ISO 8601, using the known as a Round-trip date/time pattern ("o")
// https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx#Anchor_8
// For example: 2009-06-15T13:45:30.0000000
return ((DateTimeOffset)obj).ToString (Configuration.DateTimeFormat);
else if (obj is IList)
{
var flattenedString = new StringBuilder();
foreach (var param in (IList)obj)
{
if (flattenedString.Length > 0)
flattenedString.Append(",");
flattenedString.Append(param);
}
return flattenedString.ToString();
}
else
return Convert.ToString (obj);
}
/// <summary>
/// Deserialize the JSON string into a proper object.
/// </summary>
/// <param name="response">The HTTP response.</param>
/// <param name="type">Object type.</param>
/// <returns>Object representation of the JSON string.</returns>
public object Deserialize(IRestResponse response, Type type)
{
{{^netStandard}}IList<Parameter>{{/netStandard}}{{#netStandard}}IHttpHeaders{{/netStandard}} headers = response.Headers;
if (type == typeof(byte[])) // return byte array
{
return response.RawBytes;
}
// TODO: ? if (type.IsAssignableFrom(typeof(Stream)))
if (type == typeof(Stream))
{
if (headers != null)
{
var filePath = String.IsNullOrEmpty(Configuration.TempFolderPath)
? Path.GetTempPath()
: Configuration.TempFolderPath;
var regex = new Regex(@"Content-Disposition=.*filename=['""]?([^'""\s]+)['""]?$");
foreach (var header in headers)
{
var match = regex.Match(header.ToString());
if (match.Success)
{
string fileName = filePath + SanitizeFilename(match.Groups[1].Value.Replace("\"", "").Replace("'", ""));
File.WriteAllBytes(fileName, response.RawBytes);
return new FileStream(fileName, FileMode.Open);
}
}
}
var stream = new MemoryStream(response.RawBytes);
return stream;
}
if (type.Name.StartsWith("System.Nullable`1[[System.DateTime")) // return a datetime object
{
return DateTime.Parse(response.Content, null, System.Globalization.DateTimeStyles.RoundtripKind);
}
if (type == typeof(String) || type.Name.StartsWith("System.Nullable")) // return primitive type
{
return ConvertType(response.Content, type);
}
// at this point, it must be a model (json)
try
{
return JsonConvert.DeserializeObject(response.Content, type, serializerSettings);
}
catch (Exception e)
{
throw new ApiException(500, e.Message);
}
}
/// <summary>
/// Serialize an input (model) into JSON string
/// </summary>
/// <param name="obj">Object.</param>
/// <returns>JSON string.</returns>
public String Serialize(object obj)
{
try
{
return obj != null ? JsonConvert.SerializeObject(obj) : null;
}
catch (Exception e)
{
throw new ApiException(500, e.Message);
}
}
/// <summary>
///Check if the given MIME is a JSON MIME.
///JSON MIME examples:
/// application/json
/// application/json; charset=UTF8
/// APPLICATION/JSON
/// application/vnd.company+json
/// </summary>
/// <param name="mime">MIME</param>
/// <returns>Returns True if MIME type is json.</returns>
public bool IsJsonMime(String mime)
{
var jsonRegex = new Regex("(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$");
return mime != null && (jsonRegex.IsMatch(mime) || mime.Equals("application/json-patch+json"));
}
/// <summary>
/// Select the Content-Type header's value from the given content-type array:
/// if JSON type exists in the given array, use it;
/// otherwise use the first one defined in 'consumes'
/// </summary>
/// <param name="contentTypes">The Content-Type array to select from.</param>
/// <returns>The Content-Type header to use.</returns>
public String SelectHeaderContentType(String[] contentTypes)
{
if (contentTypes.Length == 0)
return "application/json";
foreach (var contentType in contentTypes)
{
if (IsJsonMime(contentType.ToLower()))
return contentType;
}
return contentTypes[0]; // use the first content type specified in 'consumes'
}
/// <summary>
/// Select the Accept header's value from the given accepts array:
/// if JSON exists in the given array, use it;
/// otherwise use all of them (joining into a string)
/// </summary>
/// <param name="accepts">The accepts array to select from.</param>
/// <returns>The Accept header to use.</returns>
public String SelectHeaderAccept(String[] accepts)
{
if (accepts.Length == 0)
return null;
if (accepts.Contains("application/json", StringComparer.OrdinalIgnoreCase))
return "application/json";
return String.Join(",", accepts);
}
/// <summary>
/// Encode string in base64 format.
/// </summary>
/// <param name="text">String to be encoded.</param>
/// <returns>Encoded string.</returns>
public static string Base64Encode(string text)
{
return System.Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(text));
}
/// <summary>
/// Dynamically cast the object into target type.
/// </summary>
/// <param name="fromObject">Object to be casted</param>
/// <param name="toObject">Target type</param>
/// <returns>Casted object</returns>
{{#supportsAsync}}
public static dynamic ConvertType(dynamic fromObject, Type toObject)
{{/supportsAsync}}
{{^supportsAsync}}
public static object ConvertType<T>(T fromObject, Type toObject) where T : class
{{/supportsAsync}}
{
return Convert.ChangeType(fromObject, toObject);
}
/// <summary>
/// Convert stream to byte array
/// </summary>
/// <param name="inputStream">Input stream to be converted</param>
/// <returns>Byte array</returns>
public static byte[] ReadAsBytes(Stream inputStream)
{
byte[] buf = new byte[16*1024];
using (MemoryStream ms = new MemoryStream())
{
int count;
while ((count = inputStream.Read(buf, 0, buf.Length)) > 0)
{
ms.Write(buf, 0, count);
}
return ms.ToArray();
}
}
/// <summary>
/// URL encode a string
/// Credit/Ref: https://github.com/restsharp/RestSharp/blob/master/RestSharp/Extensions/StringExtensions.cs#L50
/// </summary>
/// <param name="input">String to be URL encoded</param>
/// <returns>Byte array</returns>
public static string UrlEncode(string input)
{
const int maxLength = 32766;
if (input == null)
{
throw new ArgumentNullException("input");
}
if (input.Length <= maxLength)
{
return Uri.EscapeDataString(input);
}
StringBuilder sb = new StringBuilder(input.Length * 2);
int index = 0;
while (index < input.Length)
{
int length = Math.Min(input.Length - index, maxLength);
string subString = input.Substring(index, length);
sb.Append(Uri.EscapeDataString(subString));
index += subString.Length;
}
return sb.ToString();
}
/// <summary>
/// Sanitize filename by removing the path
/// </summary>
/// <param name="filename">Filename</param>
/// <returns>Filename</returns>
public static string SanitizeFilename(string filename)
{
Match match = Regex.Match(filename, @".*[/\\](.*)$");
if (match.Success)
{
return match.Groups[1].Value;
}
else
{
return filename;
}
}
{{^netStandard}}
{{#supportsUWP}}
/// <summary>
/// Convert stream to byte array
/// </summary>
/// <param name="stream">IO stream</param>
/// <returns>Byte array</returns>
public static byte[] ToByteArray(Stream stream)
{
stream.Position = 0;
byte[] buffer = new byte[stream.Length];
for (int totalBytesCopied = 0; totalBytesCopied < stream.Length;)
totalBytesCopied += stream.Read(buffer, totalBytesCopied, Convert.ToInt32(stream.Length) - totalBytesCopied);
return buffer;
}
{{/supportsUWP}}
{{/netStandard}}
/// <summary>
/// Convert params to key/value pairs.
/// Use collectionFormat to properly format lists and collections.
/// </summary>
/// <param name="name">Key name.</param>
/// <param name="value">Value object.</param>
/// <returns>A list of KeyValuePairs</returns>
public IEnumerable<KeyValuePair<string, string>> ParameterToKeyValuePairs(string collectionFormat, string name, object value)
{
var parameters = new List<KeyValuePair<string, string>>();
if (IsCollection(value) && collectionFormat == "multi")
{
var valueCollection = value as IEnumerable;
parameters.AddRange(from object item in valueCollection select new KeyValuePair<string, string>(name, ParameterToString(item)));
}
else
{
parameters.Add(new KeyValuePair<string, string>(name, ParameterToString(value)));
}
return parameters;
}
/// <summary>
/// Check if generic object is a collection.
/// </summary>
/// <param name="value"></param>
/// <returns>True if object is a collection type</returns>
private static bool IsCollection(object value)
{
return value is IList || value is ICollection;
}
}
}

View File

@ -0,0 +1,464 @@
{{>partial_header}}
using System;
using System.Reflection;
{{^net35}}
using System.Collections.Concurrent;
{{/net35}}
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Amazon.SellingPartnerAPIAA;
namespace {{packageName}}.Client
{
/// <summary>
/// Represents a set of configuration settings
/// </summary>
{{>visibility}} class Configuration : IReadableConfiguration
{
#region Constants
/// <summary>
/// Version of the package.
/// </summary>
/// <value>Version of the package.</value>
public const string Version = "{{packageVersion}}";
/// <summary>
/// Identifier for ISO 8601 DateTime Format
/// </summary>
/// <remarks>See https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx#Anchor_8 for more information.</remarks>
// ReSharper disable once InconsistentNaming
public const string ISO8601_DATETIME_FORMAT = "o";
#endregion Constants
#region Static Members
private static readonly object GlobalConfigSync = new { };
private static Configuration _globalConfiguration;
/// <summary>
/// Default creation of exceptions for a given method name and response object
/// </summary>
public static readonly ExceptionFactory DefaultExceptionFactory = (methodName, response) =>
{
var status = (int)response.StatusCode;
if (status >= 400)
{
return new ApiException(status,
string.Format("Error calling {0}: {1}", methodName, response.Content),
response.Content);
}
{{^netStandard}}if (status == 0)
{
return new ApiException(status,
string.Format("Error calling {0}: {1}", methodName, response.ErrorMessage), response.ErrorMessage);
}{{/netStandard}}
return null;
};
/// <summary>
/// Gets or sets the default Configuration.
/// </summary>
/// <value>Configuration.</value>
public static Configuration Default
{
get { return _globalConfiguration; }
set
{
lock (GlobalConfigSync)
{
_globalConfiguration = value;
}
}
}
#endregion Static Members
#region Private Members
/// <summary>
/// Gets or sets the API key based on the authentication name.
/// </summary>
/// <value>The API key.</value>
private IDictionary<string, string> _apiKey = null;
/// <summary>
/// Gets or sets the prefix (e.g. Token) of the API key based on the authentication name.
/// </summary>
/// <value>The prefix of the API key.</value>
private IDictionary<string, string> _apiKeyPrefix = null;
private string _dateTimeFormat = ISO8601_DATETIME_FORMAT;
private string _tempFolderPath = Path.GetTempPath();
#endregion Private Members
#region Constructors
static Configuration()
{
_globalConfiguration = new GlobalConfiguration();
}
/// <summary>
/// Initializes a new instance of the <see cref="Configuration" /> class
/// </summary>
public Configuration()
{
UserAgent = "{{#httpUserAgent}}{{.}}{{/httpUserAgent}}{{^httpUserAgent}}Swagger-Codegen/{{packageVersion}}/csharp{{/httpUserAgent}}";
BasePath = "{{{basePath}}}";
DefaultHeader = new {{^net35}}Concurrent{{/net35}}Dictionary<string, string>();
ApiKey = new {{^net35}}Concurrent{{/net35}}Dictionary<string, string>();
ApiKeyPrefix = new {{^net35}}Concurrent{{/net35}}Dictionary<string, string>();
}
/// <summary>
/// Initializes a new instance of the <see cref="Configuration" /> class
/// </summary>
public Configuration(
IDictionary<string, string> defaultHeader,
IDictionary<string, string> apiKey,
IDictionary<string, string> apiKeyPrefix,
string basePath = "{{{basePath}}}") : this()
{
if (string.{{^net35}}IsNullOrWhiteSpace{{/net35}}{{#net35}}IsNullOrEmpty{{/net35}}(basePath))
throw new ArgumentException("The provided basePath is invalid.", "basePath");
if (defaultHeader == null)
throw new ArgumentNullException("defaultHeader");
if (apiKey == null)
throw new ArgumentNullException("apiKey");
if (apiKeyPrefix == null)
throw new ArgumentNullException("apiKeyPrefix");
BasePath = basePath;
foreach (var keyValuePair in defaultHeader)
{
DefaultHeader.Add(keyValuePair);
}
foreach (var keyValuePair in apiKey)
{
ApiKey.Add(keyValuePair);
}
foreach (var keyValuePair in apiKeyPrefix)
{
ApiKeyPrefix.Add(keyValuePair);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="Configuration" /> class with different settings
/// </summary>
/// <param name="apiClient">Api client</param>
/// <param name="defaultHeader">Dictionary of default HTTP header</param>
/// <param name="username">Username</param>
/// <param name="password">Password</param>
/// <param name="accessToken">accessToken</param>
/// <param name="apiKey">Dictionary of API key</param>
/// <param name="apiKeyPrefix">Dictionary of API key prefix</param>
/// <param name="tempFolderPath">Temp folder path</param>
/// <param name="dateTimeFormat">DateTime format string</param>
/// <param name="timeout">HTTP connection timeout (in milliseconds)</param>
/// <param name="userAgent">HTTP user agent</param>
[Obsolete("Use explicit object construction and setting of properties.", true)]
public Configuration(
// ReSharper disable UnusedParameter.Local
ApiClient apiClient = null,
IDictionary<string, string> defaultHeader = null,
string username = null,
string password = null,
string accessToken = null,
IDictionary<string, string> apiKey = null,
IDictionary<string, string> apiKeyPrefix = null,
string tempFolderPath = null,
string dateTimeFormat = null,
int timeout = 100000,
string userAgent = "{{#httpUserAgent}}{{.}}{{/httpUserAgent}}{{^httpUserAgent}}Swagger-Codegen/{{packageVersion}}/csharp{{/httpUserAgent}}"
// ReSharper restore UnusedParameter.Local
)
{
}
/// <summary>
/// Initializes a new instance of the Configuration class.
/// </summary>
/// <param name="apiClient">Api client.</param>
[Obsolete("This constructor caused unexpected sharing of static data. It is no longer supported.", true)]
// ReSharper disable once UnusedParameter.Local
public Configuration(ApiClient apiClient)
{
}
#endregion Constructors
#region Properties
private ApiClient _apiClient = null;
/// <summary>
/// Gets an instance of an ApiClient for this configuration
/// </summary>
public virtual ApiClient ApiClient
{
get
{
if (_apiClient == null) _apiClient = CreateApiClient();
return _apiClient;
}
}
private String _basePath = null;
/// <summary>
/// Gets or sets the base path for API access.
/// </summary>
public virtual string BasePath {
get { return _basePath; }
set {
_basePath = value;
// pass-through to ApiClient if it's set.
if(_apiClient != null) {
_apiClient.RestClient.BaseUrl = new Uri(_basePath);
}
}
}
/// <summary>
/// Gets or sets the default header.
/// </summary>
public virtual IDictionary<string, string> DefaultHeader { get; set; }
/// <summary>
/// Gets or sets the HTTP timeout (milliseconds) of ApiClient. Default to 100000 milliseconds.
/// </summary>
public virtual int Timeout
{
{{#netStandard}}get { return (int)ApiClient.RestClient.Timeout.GetValueOrDefault(TimeSpan.FromSeconds(0)).TotalMilliseconds; }
set { ApiClient.RestClient.Timeout = TimeSpan.FromMilliseconds(value); }{{/netStandard}}{{^netStandard}}
get { return ApiClient.RestClient.Timeout; }
set { ApiClient.RestClient.Timeout = value; }{{/netStandard}}
}
/// <summary>
/// Gets or sets the HTTP user agent.
/// </summary>
/// <value>Http user agent.</value>
public virtual string UserAgent { get; set; }
/// <summary>
/// Gets or sets the username (HTTP basic authentication).
/// </summary>
/// <value>The username.</value>
public virtual string Username { get; set; }
/// <summary>
/// Gets or sets the password (HTTP basic authentication).
/// </summary>
/// <value>The password.</value>
public virtual string Password { get; set; }
/// <summary>
/// Gets or sets the LWAAuthorizationCredentials for Amazon Selling Partner API Authorization
/// </summary>
/// <value>The LWAAuthorizationCredentials</value>
public virtual LWAAuthorizationCredentials AuthorizationCredentials { get; set; }
/// <summary>
/// Gets or sets the AWSAuthenticationCredentials for Amazon Selling Partner API Authentication
/// </summary>
/// <value>The AWSAuthenticationCredentials</value>
public virtual AWSAuthenticationCredentials AuthenticationCredentials { get; set; }
/// <summary>
/// Gets the API key with prefix.
/// </summary>
/// <param name="apiKeyIdentifier">API key identifier (authentication scheme).</param>
/// <returns>API key with prefix.</returns>
public string GetApiKeyWithPrefix(string apiKeyIdentifier)
{
var apiKeyValue = "";
ApiKey.TryGetValue (apiKeyIdentifier, out apiKeyValue);
var apiKeyPrefix = "";
if (ApiKeyPrefix.TryGetValue (apiKeyIdentifier, out apiKeyPrefix))
return apiKeyPrefix + " " + apiKeyValue;
else
return apiKeyValue;
}
/// <summary>
/// Gets or sets the access token for OAuth2 authentication.
/// </summary>
/// <value>The access token.</value>
public virtual string AccessToken { get; set; }
/// <summary>
/// Gets or sets the temporary folder path to store the files downloaded from the server.
/// </summary>
/// <value>Folder path.</value>
public virtual string TempFolderPath
{
get { return _tempFolderPath; }
set
{
if (string.IsNullOrEmpty(value))
{
// Possible breaking change since swagger-codegen 2.2.1, enforce a valid temporary path on set.
_tempFolderPath = Path.GetTempPath();
return;
}
// create the directory if it does not exist
if (!Directory.Exists(value))
{
Directory.CreateDirectory(value);
}
// check if the path contains directory separator at the end
if (value[value.Length - 1] == Path.DirectorySeparatorChar)
{
_tempFolderPath = value;
}
else
{
_tempFolderPath = value + Path.DirectorySeparatorChar;
}
}
}
/// <summary>
/// Gets or sets the date time format used when serializing in the ApiClient
/// By default, it's set to ISO 8601 - "o", for others see:
/// https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
/// and https://msdn.microsoft.com/en-us/library/8kb3ddd4(v=vs.110).aspx
/// No validation is done to ensure that the string you're providing is valid
/// </summary>
/// <value>The DateTimeFormat string</value>
public virtual string DateTimeFormat
{
get { return _dateTimeFormat; }
set
{
if (string.IsNullOrEmpty(value))
{
// Never allow a blank or null string, go back to the default
_dateTimeFormat = ISO8601_DATETIME_FORMAT;
return;
}
// Caution, no validation when you choose date time format other than ISO 8601
// Take a look at the above links
_dateTimeFormat = value;
}
}
/// <summary>
/// Gets or sets the prefix (e.g. Token) of the API key based on the authentication name.
/// </summary>
/// <value>The prefix of the API key.</value>
public virtual IDictionary<string, string> ApiKeyPrefix
{
get { return _apiKeyPrefix; }
set
{
if (value == null)
{
throw new InvalidOperationException("ApiKeyPrefix collection may not be null.");
}
_apiKeyPrefix = value;
}
}
/// <summary>
/// Gets or sets the API key based on the authentication name.
/// </summary>
/// <value>The API key.</value>
public virtual IDictionary<string, string> ApiKey
{
get { return _apiKey; }
set
{
if (value == null)
{
throw new InvalidOperationException("ApiKey collection may not be null.");
}
_apiKey = value;
}
}
#endregion Properties
#region Methods
/// <summary>
/// Add default header.
/// </summary>
/// <param name="key">Header field name.</param>
/// <param name="value">Header field value.</param>
/// <returns></returns>
public void AddDefaultHeader(string key, string value)
{
DefaultHeader[key] = value;
}
/// <summary>
/// Creates a new <see cref="ApiClient" /> based on this <see cref="Configuration" /> instance.
/// </summary>
/// <returns></returns>
public ApiClient CreateApiClient()
{
return new ApiClient(this);
}
/// <summary>
/// Returns a string with essential information for debugging.
/// </summary>
public static String ToDebugReport()
{
String report = "C# SDK ({{{packageName}}}) Debug Report:\n";
{{^netStandard}}
{{^supportsUWP}}
report += " OS: " + System.Environment.OSVersion + "\n";
report += " .NET Framework Version: " + System.Environment.Version + "\n";
{{/supportsUWP}}
{{/netStandard}}
{{#netStandard}}
report += " OS: " + System.Runtime.InteropServices.RuntimeInformation.OSDescription + "\n";
{{/netStandard}}
report += " Version of the API: {{{version}}}\n";
report += " SDK Package Version: {{{packageVersion}}}\n";
return report;
}
/// <summary>
/// Add Api Key Header.
/// </summary>
/// <param name="key">Api Key name.</param>
/// <param name="value">Api Key value.</param>
/// <returns></returns>
public void AddApiKey(string key, string value)
{
ApiKey[key] = value;
}
/// <summary>
/// Sets the API key prefix.
/// </summary>
/// <param name="key">Api Key name.</param>
/// <param name="value">Api Key value.</param>
public void AddApiKeyPrefix(string key, string value)
{
ApiKeyPrefix[key] = value;
}
#endregion Methods
}
}

View File

@ -0,0 +1,98 @@
{{>partial_header}}
using System.Collections.Generic;
using Amazon.SellingPartnerAPIAA;
namespace {{packageName}}.Client
{
/// <summary>
/// Represents a readable-only configuration contract.
/// </summary>
public interface IReadableConfiguration
{
/// <summary>
/// Gets the access token.
/// </summary>
/// <value>Access token.</value>
string AccessToken { get; }
/// <summary>
/// Gets the API key.
/// </summary>
/// <value>API key.</value>
IDictionary<string, string> ApiKey { get; }
/// <summary>
/// Gets the API key prefix.
/// </summary>
/// <value>API key prefix.</value>
IDictionary<string, string> ApiKeyPrefix { get; }
/// <summary>
/// Gets the base path.
/// </summary>
/// <value>Base path.</value>
string BasePath { get; }
/// <summary>
/// Gets the date time format.
/// </summary>
/// <value>Date time foramt.</value>
string DateTimeFormat { get; }
/// <summary>
/// Gets the default header.
/// </summary>
/// <value>Default header.</value>
IDictionary<string, string> DefaultHeader { get; }
/// <summary>
/// Gets the temp folder path.
/// </summary>
/// <value>Temp folder path.</value>
string TempFolderPath { get; }
/// <summary>
/// Gets the HTTP connection timeout (in milliseconds)
/// </summary>
/// <value>HTTP connection timeout.</value>
int Timeout { get; }
/// <summary>
/// Gets the user agent.
/// </summary>
/// <value>User agent.</value>
string UserAgent { get; }
/// <summary>
/// Gets the username.
/// </summary>
/// <value>Username.</value>
string Username { get; }
/// <summary>
/// Gets the password.
/// </summary>
/// <value>Password.</value>
string Password { get; }
/// <summary>
/// Gets the API key with prefix.
/// </summary>
/// <param name="apiKeyIdentifier">API key identifier (authentication scheme).</param>
/// <returns>API key with prefix.</returns>
string GetApiKeyWithPrefix(string apiKeyIdentifier);
/// <summary>
/// Gets the LWAAuthorizationCredentials for Amazon Selling Partner API Authorization
/// </summary>
/// <value>AuthorizationCredentials</value>
LWAAuthorizationCredentials AuthorizationCredentials { get; }
/// <summary>
/// Gets the AWSAuthenticationCredentials for Amazon Selling Partner API Authentication
/// </summary>
/// <value>AuthenticationCredentials</value>
AWSAuthenticationCredentials AuthenticationCredentials { get; }
}
}

View File

@ -0,0 +1,478 @@
{{>partial_header}}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
{{#netStandard}}
using RestSharp.Portable;
{{/netStandard}}
{{^netStandard}}
using RestSharp;
{{/netStandard}}
using {{packageName}}.Client;
{{#hasImport}}using {{packageName}}.{{modelPackage}};
{{/hasImport}}
using Amazon.SellingPartnerAPIAA;
namespace {{packageName}}.{{apiPackage}}
{
{{#operations}}
/// <summary>
/// Represents a collection of functions to interact with the API endpoints
/// </summary>
{{>visibility}} interface {{interfacePrefix}}{{classname}} : IApiAccessor
{
#region Synchronous Operations
{{#operation}}
/// <summary>
/// {{summary}}
/// </summary>
/// <remarks>
/// {{notes}}
/// </remarks>
/// <exception cref="{{packageName}}.Client.ApiException">Thrown when fails to make API call</exception>
{{#allParams}}/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}/// <returns>{{#returnType}}{{returnType}}{{/returnType}}</returns>
{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} {{operationId}} ({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = null{{/optionalMethodArgument}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}});
/// <summary>
/// {{summary}}
/// </summary>
/// <remarks>
/// {{notes}}
/// </remarks>
/// <exception cref="{{packageName}}.Client.ApiException">Thrown when fails to make API call</exception>
{{#allParams}}/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}/// <returns>ApiResponse of {{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}Object(void){{/returnType}}</returns>
ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Object{{/returnType}}> {{operationId}}WithHttpInfo ({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = null{{/optionalMethodArgument}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}});
{{/operation}}
#endregion Synchronous Operations
{{#supportsAsync}}
#region Asynchronous Operations
{{#operation}}
/// <summary>
/// {{summary}}
/// </summary>
/// <remarks>
/// {{notes}}
/// </remarks>
/// <exception cref="{{packageName}}.Client.ApiException">Thrown when fails to make API call</exception>
{{#allParams}}/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}/// <returns>Task of {{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}void{{/returnType}}</returns>
{{#returnType}}System.Threading.Tasks.Task<{{{returnType}}}>{{/returnType}}{{^returnType}}System.Threading.Tasks.Task{{/returnType}} {{operationId}}Async ({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = null{{/optionalMethodArgument}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}});
/// <summary>
/// {{summary}}
/// </summary>
/// <remarks>
/// {{notes}}
/// </remarks>
/// <exception cref="{{packageName}}.Client.ApiException">Thrown when fails to make API call</exception>
{{#allParams}}/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}/// <returns>Task of ApiResponse{{#returnType}} ({{returnType}}){{/returnType}}</returns>
System.Threading.Tasks.Task<ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Object{{/returnType}}>> {{operationId}}AsyncWithHttpInfo ({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = null{{/optionalMethodArgument}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}});
{{/operation}}
#endregion Asynchronous Operations
{{/supportsAsync}}
}
/// <summary>
/// Represents a collection of functions to interact with the API endpoints
/// </summary>
{{>visibility}} partial class {{classname}} : {{interfacePrefix}}{{classname}}
{
private {{packageName}}.Client.ExceptionFactory _exceptionFactory = (name, response) => null;
/// <summary>
/// Initializes a new instance of the <see cref="{{classname}}"/> class
/// using Configuration object
/// </summary>
/// <param name="configuration">An instance of Configuration</param>
/// <returns></returns>
public {{classname}}({{packageName}}.Client.Configuration configuration)
{
this.Configuration = configuration;
ExceptionFactory = {{packageName}}.Client.Configuration.DefaultExceptionFactory;
}
/// <summary>
/// Gets the base path of the API client.
/// </summary>
/// <value>The base path</value>
public String GetBasePath()
{
return this.Configuration.ApiClient.RestClient.BaseUrl.ToString();
}
/// <summary>
/// Sets the base path of the API client.
/// </summary>
/// <value>The base path</value>
[Obsolete("SetBasePath is deprecated, please do 'Configuration.ApiClient = new ApiClient(\"http://new-path\")' instead.")]
public void SetBasePath(String basePath)
{
// do nothing
}
/// <summary>
/// Gets or sets the configuration object
/// </summary>
/// <value>An instance of the Configuration</value>
public {{packageName}}.Client.Configuration Configuration {get; set;}
/// <summary>
/// Provides a factory method hook for the creation of exceptions.
/// </summary>
public {{packageName}}.Client.ExceptionFactory ExceptionFactory
{
get
{
if (_exceptionFactory != null && _exceptionFactory.GetInvocationList().Length > 1)
{
throw new InvalidOperationException("Multicast delegate for ExceptionFactory is unsupported.");
}
return _exceptionFactory;
}
set { _exceptionFactory = value; }
}
/// <summary>
/// Gets the default header.
/// </summary>
/// <returns>Dictionary of HTTP header</returns>
[Obsolete("DefaultHeader is deprecated, please use Configuration.DefaultHeader instead.")]
public IDictionary<String, String> DefaultHeader()
{
return new {{^net35}}ReadOnly{{/net35}}Dictionary<string, string>(this.Configuration.DefaultHeader);
}
/// <summary>
/// Add default header.
/// </summary>
/// <param name="key">Header field name.</param>
/// <param name="value">Header field value.</param>
/// <returns></returns>
[Obsolete("AddDefaultHeader is deprecated, please use Configuration.AddDefaultHeader instead.")]
public void AddDefaultHeader(string key, string value)
{
this.Configuration.AddDefaultHeader(key, value);
}
{{#operation}}
/// <summary>
/// {{summary}} {{notes}}
/// </summary>
/// <exception cref="{{packageName}}.Client.ApiException">Thrown when fails to make API call</exception>
{{#allParams}}/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}/// <returns>{{#returnType}}{{returnType}}{{/returnType}}</returns>
public {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} {{operationId}} ({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = null{{/optionalMethodArgument}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}})
{
{{#returnType}}ApiResponse<{{{returnType}}}> localVarResponse = {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/allParams}});
return localVarResponse.Data;{{/returnType}}{{^returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/allParams}});{{/returnType}}
}
/// <summary>
/// {{summary}} {{notes}}
/// </summary>
/// <exception cref="{{packageName}}.Client.ApiException">Thrown when fails to make API call</exception>
{{#allParams}}/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}/// <returns>ApiResponse of {{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}Object(void){{/returnType}}</returns>
public ApiResponse<{{#returnType}} {{{returnType}}} {{/returnType}}{{^returnType}}Object{{/returnType}}> {{operationId}}WithHttpInfo ({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = null{{/optionalMethodArgument}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}})
{
{{#allParams}}
{{#required}}
// verify the required parameter '{{paramName}}' is set
if ({{paramName}} == null)
throw new ApiException(400, "Missing required parameter '{{paramName}}' when calling {{classname}}->{{operationId}}");
{{/required}}
{{/allParams}}
var localVarPath = "{{#netStandard}}.{{/netStandard}}{{{path}}}";
var localVarPathParams = new Dictionary<String, String>();
var localVarQueryParams = new List<KeyValuePair<String, String>>();
var localVarHeaderParams = new Dictionary<String, String>(this.Configuration.DefaultHeader);
var localVarFormParams = new Dictionary<String, String>();
var localVarFileParams = new Dictionary<String, FileParameter>();
Object localVarPostBody = null;
// to determine the Content-Type header
String[] localVarHttpContentTypes = new String[] {
{{#consumes}}
"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}
{{/consumes}}
};
String localVarHttpContentType = this.Configuration.ApiClient.SelectHeaderContentType(localVarHttpContentTypes);
// to determine the Accept header
String[] localVarHttpHeaderAccepts = new String[] {
{{#produces}}
"{{{mediaType}}}"{{#hasMore}},{{/hasMore}}
{{/produces}}
};
String localVarHttpHeaderAccept = this.Configuration.ApiClient.SelectHeaderAccept(localVarHttpHeaderAccepts);
if (localVarHttpHeaderAccept != null)
localVarHeaderParams.Add("Accept", localVarHttpHeaderAccept);
{{#pathParams}}
if ({{paramName}} != null) localVarPathParams.Add("{{baseName}}", this.Configuration.ApiClient.ParameterToString({{paramName}})); // path parameter
{{/pathParams}}
{{#queryParams}}
if ({{paramName}} != null) localVarQueryParams.AddRange(this.Configuration.ApiClient.ParameterToKeyValuePairs("{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}", "{{baseName}}", {{paramName}})); // query parameter
{{/queryParams}}
{{#headerParams}}
if ({{paramName}} != null) localVarHeaderParams.Add("{{baseName}}", this.Configuration.ApiClient.ParameterToString({{paramName}})); // header parameter
{{/headerParams}}
{{#formParams}}
if ({{paramName}} != null) {{#isFile}}localVarFileParams.Add("{{baseName}}", this.Configuration.ApiClient.ParameterToFile("{{baseName}}", {{paramName}}));{{/isFile}}{{^isFile}}localVarFormParams.Add("{{baseName}}", this.Configuration.ApiClient.ParameterToString({{paramName}})); // form parameter{{/isFile}}
{{/formParams}}
{{#bodyParam}}
if ({{paramName}} != null && {{paramName}}.GetType() != typeof(byte[]))
{
localVarPostBody = this.Configuration.ApiClient.Serialize({{paramName}}); // http body (model) parameter
}
else
{
localVarPostBody = {{paramName}}; // byte array
}
{{/bodyParam}}
{{#authMethods}}
// authentication ({{name}}) required
{{#isApiKey}}
{{#isKeyInHeader}}
if (!String.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}")))
{
localVarHeaderParams["{{keyParamName}}"] = this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}");
}
{{/isKeyInHeader}}
{{#isKeyInQuery}}
if (!String.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}")))
{
localVarQueryParams.AddRange(this.Configuration.ApiClient.ParameterToKeyValuePairs("", "{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}")));
}
{{/isKeyInQuery}}
{{/isApiKey}}
{{#isBasic}}
// http basic authentication required
if (!String.IsNullOrEmpty(this.Configuration.Username) || !String.IsNullOrEmpty(this.Configuration.Password))
{
localVarHeaderParams["Authorization"] = "Basic " + ApiClient.Base64Encode(this.Configuration.Username + ":" + this.Configuration.Password);
}
{{/isBasic}}
{{#isOAuth}}
// oauth required
if (!String.IsNullOrEmpty(this.Configuration.AccessToken))
{
localVarHeaderParams["Authorization"] = "Bearer " + this.Configuration.AccessToken;
}
{{/isOAuth}}
{{/authMethods}}
// make the HTTP request
IRestResponse localVarResponse = (IRestResponse) this.Configuration.ApiClient.CallApi(localVarPath,
Method.{{httpMethod}}, localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarFormParams, localVarFileParams,
localVarPathParams, localVarHttpContentType);
int localVarStatusCode = (int) localVarResponse.StatusCode;
if (ExceptionFactory != null)
{
Exception exception = ExceptionFactory("{{operationId}}", localVarResponse);
if (exception != null) throw exception;
}
{{#returnType}}
return new ApiResponse<{{{returnType}}}>(localVarStatusCode,
localVarResponse.Headers.ToDictionary(x => x.{{^netStandard}}Name{{/netStandard}}{{#netStandard}}Key{{/netStandard}}, x => x.Value.ToString()),
({{{returnType}}}) this.Configuration.ApiClient.Deserialize(localVarResponse, typeof({{#returnContainer}}{{{returnContainer}}}{{/returnContainer}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}})));
{{/returnType}}
{{^returnType}}
return new ApiResponse<Object>(localVarStatusCode,
localVarResponse.Headers.ToDictionary(x => x.{{^netStandard}}Name{{/netStandard}}{{#netStandard}}Key{{/netStandard}}, x => x.Value.ToString()),
null);
{{/returnType}}
}
{{#supportsAsync}}
/// <summary>
/// {{summary}} {{notes}}
/// </summary>
/// <exception cref="{{packageName}}.Client.ApiException">Thrown when fails to make API call</exception>
{{#allParams}}/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}/// <returns>Task of {{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}void{{/returnType}}</returns>
{{#returnType}}public async System.Threading.Tasks.Task<{{{returnType}}}>{{/returnType}}{{^returnType}}public async System.Threading.Tasks.Task{{/returnType}} {{operationId}}Async ({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = null{{/optionalMethodArgument}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}})
{
{{#returnType}}ApiResponse<{{{returnType}}}> localVarResponse = await {{operationId}}AsyncWithHttpInfo({{#allParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/allParams}});
return localVarResponse.Data;{{/returnType}}{{^returnType}}await {{operationId}}AsyncWithHttpInfo({{#allParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/allParams}});{{/returnType}}
}
/// <summary>
/// {{summary}} {{notes}}
/// </summary>
/// <exception cref="{{packageName}}.Client.ApiException">Thrown when fails to make API call</exception>
{{#allParams}}/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}/// <returns>Task of ApiResponse{{#returnType}} ({{returnType}}){{/returnType}}</returns>
public async System.Threading.Tasks.Task<ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Object{{/returnType}}>> {{operationId}}AsyncWithHttpInfo ({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = null{{/optionalMethodArgument}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}})
{
{{#allParams}}
{{#required}}
// verify the required parameter '{{paramName}}' is set
if ({{paramName}} == null)
throw new ApiException(400, "Missing required parameter '{{paramName}}' when calling {{classname}}->{{operationId}}");
{{/required}}
{{/allParams}}
var localVarPath = "{{#netStandard}}.{{/netStandard}}{{{path}}}";
var localVarPathParams = new Dictionary<String, String>();
var localVarQueryParams = new List<KeyValuePair<String, String>>();
var localVarHeaderParams = new Dictionary<String, String>(this.Configuration.DefaultHeader);
var localVarFormParams = new Dictionary<String, String>();
var localVarFileParams = new Dictionary<String, FileParameter>();
Object localVarPostBody = null;
// to determine the Content-Type header
String[] localVarHttpContentTypes = new String[] {
{{#consumes}}
"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}
{{/consumes}}
};
String localVarHttpContentType = this.Configuration.ApiClient.SelectHeaderContentType(localVarHttpContentTypes);
// to determine the Accept header
String[] localVarHttpHeaderAccepts = new String[] {
{{#produces}}
"{{{mediaType}}}"{{#hasMore}},{{/hasMore}}
{{/produces}}
};
String localVarHttpHeaderAccept = this.Configuration.ApiClient.SelectHeaderAccept(localVarHttpHeaderAccepts);
if (localVarHttpHeaderAccept != null)
localVarHeaderParams.Add("Accept", localVarHttpHeaderAccept);
{{#pathParams}}
if ({{paramName}} != null) localVarPathParams.Add("{{baseName}}", this.Configuration.ApiClient.ParameterToString({{paramName}})); // path parameter
{{/pathParams}}
{{#queryParams}}
if ({{paramName}} != null) localVarQueryParams.AddRange(this.Configuration.ApiClient.ParameterToKeyValuePairs("{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}", "{{baseName}}", {{paramName}})); // query parameter
{{/queryParams}}
{{#headerParams}}
if ({{paramName}} != null) localVarHeaderParams.Add("{{baseName}}", this.Configuration.ApiClient.ParameterToString({{paramName}})); // header parameter
{{/headerParams}}
{{#formParams}}
if ({{paramName}} != null) {{#isFile}}localVarFileParams.Add("{{baseName}}", this.Configuration.ApiClient.ParameterToFile("{{baseName}}", {{paramName}}));{{/isFile}}{{^isFile}}localVarFormParams.Add("{{baseName}}", this.Configuration.ApiClient.ParameterToString({{paramName}})); // form parameter{{/isFile}}
{{/formParams}}
{{#bodyParam}}
if ({{paramName}} != null && {{paramName}}.GetType() != typeof(byte[]))
{
localVarPostBody = this.Configuration.ApiClient.Serialize({{paramName}}); // http body (model) parameter
}
else
{
localVarPostBody = {{paramName}}; // byte array
}
{{/bodyParam}}
{{#authMethods}}
// authentication ({{name}}) required
{{#isApiKey}}
{{#isKeyInHeader}}
if (!String.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}")))
{
localVarHeaderParams["{{keyParamName}}"] = this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}");
}
{{/isKeyInHeader}}
{{#isKeyInQuery}}
if (!String.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}")))
{
localVarQueryParams.AddRange(this.Configuration.ApiClient.ParameterToKeyValuePairs("", "{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}")));
}
{{/isKeyInQuery}}
{{/isApiKey}}
{{#isBasic}}
// http basic authentication required
if (!String.IsNullOrEmpty(this.Configuration.Username) || !String.IsNullOrEmpty(this.Configuration.Password))
{
localVarHeaderParams["Authorization"] = "Basic " + ApiClient.Base64Encode(this.Configuration.Username + ":" + this.Configuration.Password);
}
{{/isBasic}}
{{#isOAuth}}
// oauth required
if (!String.IsNullOrEmpty(this.Configuration.AccessToken))
{
localVarHeaderParams["Authorization"] = "Bearer " + this.Configuration.AccessToken;
}
{{/isOAuth}}
{{/authMethods}}
// make the HTTP request
IRestResponse localVarResponse = (IRestResponse) await this.Configuration.ApiClient.CallApiAsync(localVarPath,
Method.{{httpMethod}}, localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarFormParams, localVarFileParams,
localVarPathParams, localVarHttpContentType);
int localVarStatusCode = (int) localVarResponse.StatusCode;
if (ExceptionFactory != null)
{
Exception exception = ExceptionFactory("{{operationId}}", localVarResponse);
if (exception != null) throw exception;
}
{{#returnType}}
return new ApiResponse<{{{returnType}}}>(localVarStatusCode,
localVarResponse.Headers.ToDictionary(x => x.{{^netStandard}}Name{{/netStandard}}{{#netStandard}}Key{{/netStandard}}, x => x.Value.ToString()),
({{{returnType}}}) this.Configuration.ApiClient.Deserialize(localVarResponse, typeof({{#returnContainer}}{{{returnContainer}}}{{/returnContainer}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}})));
{{/returnType}}
{{^returnType}}
return new ApiResponse<Object>(localVarStatusCode,
localVarResponse.Headers.ToDictionary(x => x.{{^netStandard}}Name{{/netStandard}}{{#netStandard}}Key{{/netStandard}}, x => x.Value.ToString()),
null);
{{/returnType}}
}
{{/supportsAsync}}
{{/operation}}
public class Builder
{
private LWAAuthorizationCredentials lwaAuthorizationCredentials;
private AWSAuthenticationCredentials awsAuthenticationCredentials;
public Builder SetLWAAuthorizationCredentials(LWAAuthorizationCredentials lwaAuthorizationCredentials)
{
this.lwaAuthorizationCredentials = lwaAuthorizationCredentials;
return this;
}
public Builder SetAWSAuthenticationCredentials(AWSAuthenticationCredentials awsAuthenticationCredentials)
{
this.awsAuthenticationCredentials = awsAuthenticationCredentials;
return this;
}
public {{classname}} Build()
{
if (lwaAuthorizationCredentials == null)
{
throw new NullReferenceException("LWAAuthoriztionCredentials not set");
}
if (awsAuthenticationCredentials == null)
{
throw new NullReferenceException("AWSAuthenticationCredentials not set");
}
{{packageName}}.Client.Configuration configuration = new {{packageName}}.Client.Configuration()
{
AuthorizationCredentials = lwaAuthorizationCredentials,
AuthenticationCredentials = awsAuthenticationCredentials
};
// default HTTP connection timeout (in milliseconds)
configuration.Timeout = 100000;
return new {{classname}}(configuration);
}
}
}
{{/operations}}
}

View File

@ -0,0 +1,76 @@
{{>partial_header}}
using System;
using System.IO;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using RestSharp;
using NUnit.Framework;
using {{packageName}}.Client;
using {{packageName}}.{{apiPackage}};
{{#hasImport}}using {{packageName}}.{{modelPackage}};
{{/hasImport}}
namespace {{packageName}}.Test
{
/// <summary>
/// Class for testing {{classname}}
/// </summary>
/// <remarks>
/// This file is automatically generated by Swagger Codegen.
/// Please update the test case below to test the API endpoint.
/// </remarks>
[TestFixture]
public class {{classname}}Tests
{
private {{classname}} instance;
/// <summary>
/// Setup before each unit test
/// </summary>
[SetUp]
public void Init()
{
// TODO uncomment below to initialize instance for testing
//instance = new {{classname}}();
}
/// <summary>
/// Clean up after each unit test
/// </summary>
[TearDown]
public void Cleanup()
{
}
/// <summary>
/// Test an instance of {{classname}}
/// </summary>
[Test]
public void {{operationId}}InstanceTest()
{
// TODO uncomment below to test 'IsInstanceOfType' {{classname}}
//Assert.IsInstanceOfType(typeof({{classname}}), instance, "instance is a {{classname}}");
}
{{#operations}}{{#operation}}
/// <summary>
/// Test {{operationId}}
/// </summary>
[Test]
public void {{operationId}}Test()
{
// TODO uncomment below to test the method and replace null with proper value
{{#allParams}}
//{{{dataType}}} {{paramName}} = null;
{{/allParams}}
//{{#returnType}}var response = {{/returnType}}instance.{{operationId}}({{#allParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/allParams}});
{{#returnType}}//Assert.IsInstanceOf<{{{returnType}}}> (response, "response is {{{returnType}}}");{{/returnType}}
}
{{/operation}}{{/operations}}
}
}

View File

@ -0,0 +1,88 @@
using Moq;
using Xunit;
using RestSharp;
using Amazon.SellingPartnerAPIAA;
using System;
namespace Amazon.SellingPartnerAPIAATests
{
public class AWSSigV4SignerTest
{
private const string TestAccessKeyId = "aKey";
private const string TestSecretKey = "sKey";
private const string TestRegion = "us-east-1";
private const string TestResourcePath = "iam/user";
private const string TestHost = "sellingpartnerapi.amazon.com";
private RestRequest request;
private AWSSigV4Signer sigV4SignerUnderTest;
private Mock<AWSSignerHelper> mockAWSSignerHelper;
public AWSSigV4SignerTest()
{
request = new RestRequest(TestResourcePath, Method.GET);
AWSAuthenticationCredentials authenticationCredentials = new AWSAuthenticationCredentials
{
AccessKeyId = TestAccessKeyId,
SecretKey = TestSecretKey,
Region = TestRegion
};
mockAWSSignerHelper = new Mock<AWSSignerHelper>();
sigV4SignerUnderTest = new AWSSigV4Signer(authenticationCredentials);
sigV4SignerUnderTest.AwsSignerHelper = mockAWSSignerHelper.Object;
}
[Fact]
public void TestSignRequest()
{
DateTime signingDate = DateTime.UtcNow;
string expectedHashedCanonicalRequest = "b7a5ea4c3179fcebed77f19ccd7d85795d4b7a1810709b55fa7ad3fd79ab6adc";
string expectedSignedHeaders = "testSignedHeaders";
string expectedSignature = "testSignature";
string expectedStringToSign = "testStringToSign";
mockAWSSignerHelper.Setup(signerHelper => signerHelper.InitializeHeaders(request, TestHost))
.Returns(signingDate);
mockAWSSignerHelper.Setup(signerHelper => signerHelper.ExtractCanonicalURIParameters(request.Resource))
.Returns("testURIParameters");
mockAWSSignerHelper.Setup(signerHelper => signerHelper.ExtractCanonicalQueryString(request))
.Returns("testCanonicalQueryString");
mockAWSSignerHelper.Setup(signerHelper => signerHelper.ExtractCanonicalHeaders(request))
.Returns("testCanonicalHeaders");
mockAWSSignerHelper.Setup(signerHelper => signerHelper.ExtractSignedHeaders(request))
.Returns(expectedSignedHeaders);
mockAWSSignerHelper.Setup(signerHelper => signerHelper.HashRequestBody(request))
.Returns("testHashRequestBody");
mockAWSSignerHelper.Setup(signerHelper => signerHelper.BuildStringToSign(signingDate,
expectedHashedCanonicalRequest, TestRegion))
.Returns(expectedStringToSign);
mockAWSSignerHelper.Setup(signerHelper => signerHelper.CalculateSignature(expectedStringToSign,
signingDate, TestSecretKey, TestRegion))
.Returns(expectedSignature);
IRestRequest actualRestRequest = sigV4SignerUnderTest.Sign(request, TestHost);
mockAWSSignerHelper.Verify(signerHelper => signerHelper.InitializeHeaders(request, TestHost));
mockAWSSignerHelper.Verify(signerHelper => signerHelper.ExtractCanonicalURIParameters(request.Resource));
mockAWSSignerHelper.Verify(signerHelper => signerHelper.ExtractCanonicalQueryString(request));
mockAWSSignerHelper.Verify(signerHelper => signerHelper.ExtractCanonicalHeaders(request));
mockAWSSignerHelper.Verify(signerHelper => signerHelper.ExtractSignedHeaders(request));
mockAWSSignerHelper.Verify(signerHelper => signerHelper.HashRequestBody(request));
mockAWSSignerHelper.Verify(signerHelper => signerHelper.BuildStringToSign(signingDate,
expectedHashedCanonicalRequest,
TestRegion));
mockAWSSignerHelper.Verify(signerHelper => signerHelper.CalculateSignature(expectedStringToSign,
signingDate,
TestSecretKey,
TestRegion));
mockAWSSignerHelper.Verify(signerHelper => signerHelper.AddSignature(request,
TestAccessKeyId,
expectedSignedHeaders,
expectedSignature,
TestRegion,
signingDate));
Assert.Equal(request, actualRestRequest);
}
}
}

View File

@ -0,0 +1,269 @@
using System;
using Xunit;
using RestSharp;
using Amazon.SellingPartnerAPIAA;
using System.Text;
using Moq;
namespace Amazon.SellingPartnerAPIAATests
{
public class AWSSignerHelperTest
{
private const string Slash = "/";
private const string ISOSigningDateTime = "20200504T121212Z";
private const string ISOSigningDate = "20200504";
private const string TestAccessKeyId = "aKey";
private const string TestSecretKey = "sKey";
private const string TestRegion = "us-east-1";
private const string TestResourcePath = "iam/user";
private const string TestHost = "sellingpartnerapi.amazon.com";
private const string JsonMediaType = "application/json; charset=utf-8";
private static readonly DateTime SigningDate = DateTime.Parse("2020-05-04 12:12:12");
private AWSSignerHelper awsSignerHelperUnderTest;
public AWSSignerHelperTest()
{
var mockDateHelper = new Mock<IDateHelper>();
mockDateHelper.Setup(dateHelper => dateHelper.GetUtcNow()).Returns(SigningDate);
awsSignerHelperUnderTest = new AWSSignerHelper() { DateHelper = mockDateHelper.Object };
}
[Fact]
public void TestExtractCanonicalURIParameters()
{
IRestRequest request = new RestRequest(TestResourcePath, Method.GET);
string result = awsSignerHelperUnderTest.ExtractCanonicalURIParameters(request.Resource);
Assert.Equal("/iam/user", result);
}
[Fact]
public void TestExtractCanonicalURIParameters_ResourcePathWithSpace()
{
string result = awsSignerHelperUnderTest.ExtractCanonicalURIParameters("iam/ user");
Assert.Equal("/iam/%2520user", result);
}
[Fact]
public void TestExtractCanonicalURIParameters_EmptyResourcePath()
{
string result = awsSignerHelperUnderTest.ExtractCanonicalURIParameters(string.Empty);
Assert.Equal(Slash, result);
}
[Fact]
public void TestExtractCanonicalURIParameters_NullResourcePath()
{
string result = awsSignerHelperUnderTest.ExtractCanonicalURIParameters(null);
Assert.Equal(Slash, result);
}
[Fact]
public void TestExtractCanonicalURIParameters_SlashPath()
{
string result = awsSignerHelperUnderTest.ExtractCanonicalURIParameters(Slash);
Assert.Equal(Slash, result);
}
[Fact]
public void TestExtractCanonicalQueryString()
{
IRestRequest request = new RestRequest();
request.AddQueryParameter("Version", "2010-05-08");
request.AddQueryParameter("Action", "ListUsers");
request.AddQueryParameter("RequestId", "1");
string result = awsSignerHelperUnderTest.ExtractCanonicalQueryString(request);
//Query parameters in canonical order
Assert.Equal("Action=ListUsers&RequestId=1&Version=2010-05-08", result);
}
[Fact]
public void TestExtractCanonicalQueryString_EmptyQueryParameters()
{
string result = awsSignerHelperUnderTest.ExtractCanonicalQueryString(new RestRequest());
Assert.Empty(result);
}
[Fact]
public void TestExtractCanonicalQueryString_WithUrlEncoding()
{
IRestRequest request = new RestRequest();
request.AddQueryParameter("Action^", "ListUsers$Roles");
string result = awsSignerHelperUnderTest.ExtractCanonicalQueryString(request);
Assert.Equal("Action%5E=ListUsers%24Roles", result);
}
[Fact]
public void TestExtractCanonicalHeaders()
{
IRestRequest request = new RestRequest();
request.AddHeader("X-Amz-Date", "20150830T123600Z");
request.AddHeader("Host", "iam.amazonaws.com");
request.AddHeader("Content-Type", JsonMediaType);
string result = awsSignerHelperUnderTest.ExtractCanonicalHeaders(request);
Assert.Equal("content-type:application/json; charset=utf-8\nhost:iam.amazonaws.com\n" +
"x-amz-date:20150830T123600Z\n", result);
}
[Fact]
public void TestExtractCanonicalHeaders_NoHeader()
{
string result = awsSignerHelperUnderTest.ExtractCanonicalHeaders(new RestRequest());
Assert.Empty(result);
}
[Fact]
public void TestExtractSignedHeaders()
{
IRestRequest request = new RestRequest();
request.AddHeader("X-Amz-Date", "20150830T123600Z");
request.AddHeader("Host", "iam.amazonaws.com");
request.AddHeader("Content-Type", JsonMediaType);
string result = awsSignerHelperUnderTest.ExtractSignedHeaders(request);
Assert.Equal("content-type;host;x-amz-date", result);
}
[Fact]
public void TestExtractSignedHeaders_NoHeader()
{
string result = awsSignerHelperUnderTest.ExtractSignedHeaders(new RestRequest());
Assert.Empty(result);
}
[Fact]
public void TestHashRequestBody()
{
IRestRequest request = new RestRequest(TestResourcePath, Method.POST);
request.AddJsonBody("{\"test\":\"payload\"}");
string result = awsSignerHelperUnderTest.HashRequestBody(request);
Assert.NotEmpty(result);
}
[Fact]
public void TestHashRequestBody_NoBody()
{
string result = awsSignerHelperUnderTest.HashRequestBody(new RestRequest());
Assert.NotEmpty(result);
}
[Fact]
public void TestBuildStringToSign()
{
string expectedCanonicalHash = "foo";
StringBuilder expectedStringBuilder = new StringBuilder();
expectedStringBuilder.AppendLine("AWS4-HMAC-SHA256");
expectedStringBuilder.AppendLine(ISOSigningDateTime);
expectedStringBuilder.AppendFormat("{0}/{1}/execute-api/aws4_request\n", ISOSigningDate, TestRegion);
expectedStringBuilder.Append(expectedCanonicalHash);
string result = awsSignerHelperUnderTest.BuildStringToSign(SigningDate, expectedCanonicalHash, TestRegion);
Assert.Equal(expectedStringBuilder.ToString(), result);
}
[Fact]
public void TestInitializeHeadersReturnsUtcNow()
{
Assert.Equal(SigningDate, awsSignerHelperUnderTest.InitializeHeaders(new RestRequest(), TestHost));
}
[Fact]
public void TestInitializeHeadersSetsUtcNowXAmzDateHeader()
{
IRestRequest request = new RestRequest();
awsSignerHelperUnderTest.InitializeHeaders(request, TestHost);
Parameter actualParameter = request.Parameters.Find(parameter =>
ParameterType.HttpHeader.Equals(parameter.Type) && parameter.Name == AWSSignerHelper.XAmzDateHeaderName);
Assert.Equal(ISOSigningDateTime, actualParameter.Value);
}
[Fact]
public void TestInitializeHeadersOverwritesXAmzDateHeader()
{
IRestRequest request = new RestRequest();
request.AddHeader(AWSSignerHelper.XAmzDateHeaderName, "foobar");
awsSignerHelperUnderTest.InitializeHeaders(request, TestHost);
Parameter actualParameter = request.Parameters.Find(parameter =>
ParameterType.HttpHeader.Equals(parameter.Type) && parameter.Name == AWSSignerHelper.XAmzDateHeaderName);
Assert.Equal(ISOSigningDateTime, actualParameter.Value);
}
[Fact]
public void TestAddSignatureToRequest()
{
IRestRequest restRequest = new RestRequest();
string expectedAccessKeyId = TestAccessKeyId;
string expectedRegion = TestRegion;
string expectedSignature = "testCalculatedSignature";
string expectedSignedHeaders = "header1;header2";
string expectedAuthorizationHeaderValue = string.Format("AWS4-HMAC-SHA256 " +
"Credential={0}/{1}/{2}/execute-api/aws4_request, SignedHeaders={3}, Signature={4}",
expectedAccessKeyId,
ISOSigningDate,
expectedRegion,
expectedSignedHeaders,
expectedSignature);
awsSignerHelperUnderTest.AddSignature(restRequest,
expectedAccessKeyId,
expectedSignedHeaders,
expectedSignature,
expectedRegion,
SigningDate);
Parameter actualParameter = restRequest.Parameters.Find(parameter =>
ParameterType.HttpHeader.Equals(parameter.Type) && parameter.Name == AWSSignerHelper.AuthorizationHeaderName);
Assert.Equal(expectedAuthorizationHeaderValue, actualParameter.Value);
}
[Fact]
public void TestCalculateSignature()
{
string signature = awsSignerHelperUnderTest.CalculateSignature("testString",
SigningDate,
TestSecretKey,
TestRegion);
Assert.Equal("7e2c7c2e330123ef7468b41d8ddaf3841e6ef56959b9116b44ded5466cf96405", signature);
}
[Fact]
public void TestInitializeHeadersSetsHostHeader()
{
IRestRequest restRequest = new RestRequest();
awsSignerHelperUnderTest.InitializeHeaders(restRequest, TestHost);
Parameter actualParamter = restRequest.Parameters.Find(parameter =>
ParameterType.HttpHeader.Equals(parameter.Type) && parameter.Name == AWSSignerHelper.HostHeaderName);
Assert.Equal(TestHost, actualParamter.Value);
}
[Fact]
public void TestInitializeHeadersOverwritesHostHeader()
{
IRestRequest restRequest = new RestRequest();
restRequest.AddHeader(AWSSignerHelper.HostHeaderName, "foobar");
awsSignerHelperUnderTest.InitializeHeaders(restRequest, TestHost);
Parameter actualParamter = restRequest.Parameters.Find(parameter =>
ParameterType.HttpHeader.Equals(parameter.Type) && parameter.Name == AWSSignerHelper.HostHeaderName);
Assert.Equal(TestHost, actualParamter.Value);
}
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
<PackageReference Include="Moq" Version="4.14.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Amazon.SellingPartnerAPIAA\Amazon.SellingPartnerAPIAA.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using Amazon.SellingPartnerAPIAA;
using Xunit;
namespace Amazon.SellingPartnerAPIAATests
{
public class LWAAccessTokenRequestMetaBuilderTest
{
private const string TestClientId = "cid";
private const string TestClientSecret = "csecret";
private const string TestRefreshToken = "rtoken";
private static readonly Uri TestUri = new Uri("https://www.amazon.com");
private LWAAccessTokenRequestMetaBuilder lwaAccessTokenRequestMetaBuilderUnderTest;
public LWAAccessTokenRequestMetaBuilderTest()
{
lwaAccessTokenRequestMetaBuilderUnderTest = new LWAAccessTokenRequestMetaBuilder();
}
[Fact]
public void LWAAuthorizationCredentialsWithoutScopesBuildsSellerTokenRequestMeta()
{
LWAAuthorizationCredentials lwaAuthorizationCredentials = new LWAAuthorizationCredentials()
{
ClientId = TestClientId,
ClientSecret = TestClientSecret,
Endpoint = TestUri,
RefreshToken = TestRefreshToken
};
LWAAccessTokenRequestMeta expected = new LWAAccessTokenRequestMeta()
{
ClientId = TestClientId,
ClientSecret = TestClientSecret,
GrantType = LWAAccessTokenRequestMetaBuilder.SellerAPIGrantType,
RefreshToken = TestRefreshToken,
Scope = null
};
LWAAccessTokenRequestMeta actual = lwaAccessTokenRequestMetaBuilderUnderTest.Build(lwaAuthorizationCredentials);
Assert.Equal(expected, actual);
}
[Fact]
public void LWAAuthorizationCredentialsWithScopesBuildsSellerlessTokenRequestMeta()
{
LWAAuthorizationCredentials lwaAuthorizationCredentials = new LWAAuthorizationCredentials()
{
ClientId = TestClientId,
ClientSecret = TestClientSecret,
Endpoint = TestUri,
Scopes = new List<string>() { ScopeConstants.ScopeMigrationAPI, ScopeConstants.ScopeNotificationsAPI }
};
LWAAccessTokenRequestMeta expected = new LWAAccessTokenRequestMeta()
{
ClientId = TestClientId,
ClientSecret = TestClientSecret,
GrantType = LWAAccessTokenRequestMetaBuilder.SellerlessAPIGrantType,
Scope = string.Format("{0} {1}", ScopeConstants.ScopeMigrationAPI, ScopeConstants.ScopeNotificationsAPI),
RefreshToken = null
};
LWAAccessTokenRequestMeta actual = lwaAccessTokenRequestMetaBuilderUnderTest.Build(lwaAuthorizationCredentials);
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using Xunit;
using Moq;
using RestSharp;
using Amazon.SellingPartnerAPIAA;
namespace Amazon.SellingPartnerAPIAATests
{
public class LWAAuthorizationSignerTest
{
private static readonly LWAAuthorizationCredentials LWAAuthorizationCredentials = new LWAAuthorizationCredentials()
{
ClientId = "cid",
ClientSecret = "csecret",
Endpoint = new Uri("https://www.amazon.com")
};
private LWAAuthorizationSigner lwaAuthorizationSignerUnderTest;
public LWAAuthorizationSignerTest()
{
lwaAuthorizationSignerUnderTest = new LWAAuthorizationSigner(LWAAuthorizationCredentials);
}
[Fact]
public void ConstructorInitializesLWAClientWithCredentials()
{
Assert.Equal(LWAAuthorizationCredentials, lwaAuthorizationSignerUnderTest.LWAClient.LWAAuthorizationCredentials);
}
[Fact]
public void RequestIsSignedFromLWAClientProvidedToken()
{
string expectedAccessToken = "foo";
var mockLWAClient = new Mock<LWAClient>(LWAAuthorizationCredentials);
mockLWAClient.Setup(lwaClient => lwaClient.GetAccessToken()).Returns(expectedAccessToken);
lwaAuthorizationSignerUnderTest.LWAClient = mockLWAClient.Object;
IRestRequest restRequest = new RestRequest();
restRequest = lwaAuthorizationSignerUnderTest.Sign(restRequest);
Parameter actualAccessTokenHeader = restRequest.Parameters.Find(parameter =>
ParameterType.HttpHeader.Equals(parameter.Type) && parameter.Name == LWAAuthorizationSigner.AccessTokenHeaderName);
Assert.Equal(expectedAccessToken, actualAccessTokenHeader.Value);
}
}
}

View File

@ -0,0 +1,148 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using Moq;
using Newtonsoft.Json.Linq;
using RestSharp;
using Amazon.SellingPartnerAPIAA;
using Xunit;
namespace Amazon.SellingPartnerAPIAATests
{
public class LWAClientTest
{
private const string TestClientSecret = "cSecret";
private const string TestClientId = "cId";
private const string TestRefreshGrantType = "rToken";
private Mock<RestClient> mockRestClient;
private Mock<LWAAccessTokenRequestMetaBuilder> mockLWAAccessTokenRequestMetaBuilder;
private static readonly Uri TestEndpoint = new Uri("https://www.amazon.com/lwa");
private static readonly LWAAuthorizationCredentials LWAAuthorizationCredentials = new LWAAuthorizationCredentials
{
ClientId = TestClientId,
ClientSecret = TestClientSecret,
RefreshToken = TestRefreshGrantType,
Endpoint = TestEndpoint
};
private static readonly IRestResponse Response = new RestResponse
{
StatusCode = HttpStatusCode.OK,
ResponseStatus = ResponseStatus.Completed,
Content = @"{access_token:'Azta|foo'}"
};
public LWAClientTest()
{
mockRestClient = new Mock<RestClient>();
mockLWAAccessTokenRequestMetaBuilder = new Mock<LWAAccessTokenRequestMetaBuilder>();
}
[Fact]
public void InitializeLWAAuthorizationCredentials()
{
LWAClient lwaClientUnderTest = new LWAClient(LWAAuthorizationCredentials);
Assert.Equal(LWAAuthorizationCredentials, lwaClientUnderTest.LWAAuthorizationCredentials);
}
[Fact]
public void MakeRequestFromMeta()
{
IRestRequest request = new RestRequest();
LWAAccessTokenRequestMeta expectedLWAAccessTokenRequestMeta = new LWAAccessTokenRequestMeta()
{
ClientSecret = "expectedSecret",
ClientId = "expectedClientId",
RefreshToken = "expectedRefreshToken",
GrantType = "expectedGrantType"
};
mockRestClient.Setup(client => client.Execute(It.IsAny<IRestRequest>()))
.Callback((IRestRequest req) => { request = req; })
.Returns(Response);
mockLWAAccessTokenRequestMetaBuilder.Setup(builder => builder.Build(LWAAuthorizationCredentials))
.Returns(expectedLWAAccessTokenRequestMeta);
LWAClient lwaClientUnderTest = new LWAClient(LWAAuthorizationCredentials);
lwaClientUnderTest.RestClient = mockRestClient.Object;
lwaClientUnderTest.LWAAccessTokenRequestMetaBuilder = mockLWAAccessTokenRequestMetaBuilder.Object;
lwaClientUnderTest.GetAccessToken();
Parameter requestBody = request.Parameters
.FirstOrDefault(parameter => parameter.Type.Equals(ParameterType.RequestBody));
JObject jsonRequestBody = JObject.Parse(requestBody.Value.ToString());
Assert.Equal(Method.POST, request.Method);
Assert.Equal(TestEndpoint.AbsolutePath, request.Resource);
Assert.Equal(expectedLWAAccessTokenRequestMeta.RefreshToken, jsonRequestBody.GetValue("refresh_token"));
Assert.Equal(expectedLWAAccessTokenRequestMeta.GrantType, jsonRequestBody.GetValue("grant_type"));
Assert.Equal(expectedLWAAccessTokenRequestMeta.ClientId, jsonRequestBody.GetValue("client_id"));
Assert.Equal(expectedLWAAccessTokenRequestMeta.ClientSecret, jsonRequestBody.GetValue("client_secret"));
}
[Fact]
public void ReturnAccessTokenFromResponse()
{
IRestRequest request = new RestRequest();
mockRestClient.Setup(client => client.Execute(It.IsAny<IRestRequest>()))
.Callback((IRestRequest req) => { request = req; })
.Returns(Response);
LWAClient lwaClientUnderTest = new LWAClient(LWAAuthorizationCredentials);
lwaClientUnderTest.RestClient = mockRestClient.Object;
string accessToken = lwaClientUnderTest.GetAccessToken();
Assert.Equal("Azta|foo", accessToken);
}
[Fact]
public void UnsuccessfulPostThrowsException()
{
IRestResponse response = new RestResponse
{
StatusCode = HttpStatusCode.BadRequest,
ResponseStatus = ResponseStatus.Completed,
Content = string.Empty
};
IRestRequest request = new RestRequest();
mockRestClient.Setup(client => client.Execute(It.IsAny<IRestRequest>()))
.Callback((IRestRequest req) => { request = req; })
.Returns(response);
LWAClient lwaClientUnderTest = new LWAClient(LWAAuthorizationCredentials);
lwaClientUnderTest.RestClient = mockRestClient.Object;
SystemException systemException = Assert.Throws<SystemException>(() => lwaClientUnderTest.GetAccessToken());
Assert.IsType<IOException>(systemException.GetBaseException());
}
[Fact]
public void MissingAccessTokenInResponseThrowsException()
{
IRestResponse response = new RestResponse
{
StatusCode = HttpStatusCode.OK,
ResponseStatus = ResponseStatus.Completed,
Content = string.Empty
};
IRestRequest request = new RestRequest();
mockRestClient.Setup(client => client.Execute(It.IsAny<RestRequest>()))
.Callback((IRestRequest req) => { request = (RestRequest)req; })
.Returns(response);
LWAClient lwaClientUnderTest = new LWAClient(LWAAuthorizationCredentials);
lwaClientUnderTest.RestClient = mockRestClient.Object;
Assert.Throws<SystemException>(() => lwaClientUnderTest.GetAccessToken());
}
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Linq;
using System.Text;
using Xunit;
using Amazon.SellingPartnerAPIAA;
namespace Amazon.SellingPartnerAPIAATests
{
public class UtilsTest
{
private const string TestString = "test";
[Fact]
public void TestUrlEncode_WithoutEncoding()
{
string result = Utils.UrlEncode("Test-_.~");
Assert.Equal("Test-_.~", result);
}
[Fact]
public void TestUrlEncode_WithEncoding()
{
string result = Utils.UrlEncode("Test$%*^");
Assert.Equal("Test%24%25%2A%5E", result);
}
[Fact]
public void TestUrlEncode_Empty()
{
Assert.Empty(Utils.UrlEncode(string.Empty));
}
[Fact]
public void TestHash()
{
Assert.NotEmpty(Utils.Hash(TestString));
}
[Fact]
public void TestToHex()
{
string result = Utils.ToHex(Encoding.UTF8.GetBytes(TestString));
Assert.Equal("74657374", result);
}
[Fact]
public void TestGetKeyedHash()
{
byte[] expectedHash = new byte[] { 106, 120, 238, 51, 86, 30, 87, 173, 232, 197, 95, 132,155,
183, 80, 81, 25, 213, 212, 241, 218, 201, 168, 17, 253, 143, 54, 226, 42, 118, 61, 54 };
byte[] keyedHash = Utils.GetKeyedHash(Encoding.UTF8.GetBytes("testKey"), TestString);
Assert.True(expectedHash.SequenceEqual(keyedHash));
}
}
}

View File

@ -0,0 +1,9 @@
*.iml
.idea/
target/
out/
.settings/
.DS_Store
.classpath
.project

View File

@ -0,0 +1,90 @@
# Selling Partner API Authentication/Authorization Library
This library provides helper classes for use when signing HTTP requests for Amazon Selling Partner APIs. It is intended for use
with the Selling Partner API Client Libraries generated by [swagger codegen](https://swagger.io/tools/swagger-codegen/)
using the OkHttp library. It can also be integrated into custom projects.
## LWAAuthorizationSigner
Obtains and signs a request with an access token from LWA (Login with Amazon) for the specified endpoint using the provided LWA credentials.
*Example*
```
com.squareup.okhttp.Request request = new Request.Builder()
.url(...)
...
.build();
// Seller APIs
LWAAuthorizationCredentials lwaAuthorizationCredentials = LWAAuthorizationCredentials.builder()
.clientId("...")
.clientSecret("...")
.refreshToken("...")
.endpoint("...")
.build();
/* Sellerless APIs
The Selling Partner API scopes can be retrieved from the ScopeConstants class and passed as argument(s) to either the withScope(String scope) or withScopes(String... scopes) method during lwaAuthorizationCredentials object instantiation. */
import static com.amazon.SellingPartnerAPIAA.ScopeConstants.SCOPE_NOTIFICATIONS_API;
LWAAuthorizationCredentials lwaAuthorizationCredentials = LWAAuthorizationCredentials.builder()
.clientId("...")
.clientSecret("...")
.withScopes("...")
.endpoint("...")
.build();
com.squareup.okhttp.Request signedRequest = new LWAAuthorizationSigner(lwaAuthorizationCredentials)
.sign(request);
```
## AWSSigV4Signer
Signs a request with [AWS Signature Version 4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html)
using the provided AWS developer account credentials.
*Example*
```
com.squareup.okhttp.Request request = new Request.Builder()
.url(...)
...
.build();
AWSAuthenticationCredentials awsAuthenticationCredentials = AWSAuthenticationCredentials.builder()
.accessKeyId("...")
.secretKey("...")
.region("...")
.build();
com.squareup.okhttp.Request signedRequest = new AWSSigV4Signer(awsAuthenticationCredentials)
.sign(request);
```
## Resources
This package features Mustache templates designed for use with [swagger codegen](https://swagger.io/tools/swagger-codegen/).
When you build Selling Partner API Swagger models with these templates, they help generate a rich SDK with functionality to invoke Selling Partner APIs built in. The templates are located in *resources/swagger-codegen*.
## Building
This library can be built using Maven by running this command in the package root:
```
mvn clean package
```
Dependencies are declared in the pom.xml file.
## License
Swagger Codegen templates are subject to the [Swagger Codegen License](https://github.com/swagger-api/swagger-codegen#license).
All other work licensed as follows:
Copyright 2019 Amazon.com, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this library except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.amazon.sellingpartnerapi</groupId>
<artifactId>sellingpartnerapi-aa-java</artifactId>
<packaging>jar</packaging>
<version>1.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>build-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
<sourceDirectory>src</sourceDirectory>
<directory>target</directory>
<finalName>${project.artifactId}-${project.version}</finalName>
</build>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-signer -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-signer</artifactId>
<version>1.11.610</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.squareup.okhttp/okhttp -->
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
<version>2.7.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.0.0</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.3.2</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-migrationsupport -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-migrationsupport</artifactId>
<version>5.5.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.0.0</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.9</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,54 @@
{{>licenseInfo}}
package {{invokerPackage}};
{{>generatedAnnotation}}
public class StringUtil {
/**
* Check if the given array contains the given value (with case-insensitive comparison).
*
* @param array The array
* @param value The value to search
* @return true if the array contains the value
*/
public static boolean containsIgnoreCase(String[] array, String value) {
for (String str : array) {
if (value == null && str == null) return true;
if (value != null && value.equalsIgnoreCase(str)) return true;
}
return false;
}
/**
* Join an array of strings with the given separator.
* <p>
* Note: This might be replaced by utility method from commons-lang or guava someday
* if one of those libraries is added as dependency.
* </p>
*
* @param array The array of strings
* @param separator The separator
* @return the resulting string
*/
public static String join(String[] array, String separator) {
int len = array.length;
if (len == 0) return "";
StringBuilder out = new StringBuilder();
out.append(array[0]);
for (int i = 1; i < len; i++) {
out.append(separator).append(array[i]);
}
return out.toString();
}
/**
* Check if the given value is null or an empty string
*
* @param value The value to check
* @return true if the value is null or empty
*/
public static boolean isEmpty(String value) {
return value == null || value.isEmpty();
}
}

View File

@ -0,0 +1,317 @@
{{>licenseInfo}}
package {{package}};
import {{invokerPackage}}.ApiCallback;
import {{invokerPackage}}.ApiClient;
import {{invokerPackage}}.ApiException;
import {{invokerPackage}}.ApiResponse;
import {{invokerPackage}}.Configuration;
import {{invokerPackage}}.Pair;
import {{invokerPackage}}.ProgressRequestBody;
import {{invokerPackage}}.ProgressResponseBody;
import {{invokerPackage}}.StringUtil;
{{#performBeanValidation}}
import {{invokerPackage}}.BeanValidationException;
{{/performBeanValidation}}
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
{{#useBeanValidation}}
import javax.validation.constraints.*;
{{/useBeanValidation}}
{{#performBeanValidation}}
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import javax.validation.executable.ExecutableValidator;
import java.util.Set;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
{{/performBeanValidation}}
{{#imports}}import {{import}};
{{/imports}}
import java.lang.reflect.Type;
{{^fullJavaUtil}}
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
{{/fullJavaUtil}}
import com.amazon.SellingPartnerAPIAA.AWSSigV4Signer;
import com.amazon.SellingPartnerAPIAA.LWAAuthorizationSigner;
import com.amazon.SellingPartnerAPIAA.LWAAuthorizationCredentials;
import com.amazon.SellingPartnerAPIAA.AWSAuthenticationCredentials;
{{#operations}}
public class {{classname}} {
private ApiClient {{localVariablePrefix}}apiClient;
{{classname}}() {
this(Configuration.getDefaultApiClient());
}
public {{classname}}(ApiClient apiClient) {
this.{{localVariablePrefix}}apiClient = apiClient;
}
public ApiClient getApiClient() {
return {{localVariablePrefix}}apiClient;
}
public void setApiClient(ApiClient apiClient) {
this.{{localVariablePrefix}}apiClient = apiClient;
}
{{#operation}}
/**
* Build call for {{operationId}}{{#allParams}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{/allParams}}
* @param progressListener Progress listener
* @param progressRequestListener Progress request listener
* @return Call to execute
* @throws ApiException If fail to serialize the request body object
{{#isDeprecated}}
* @deprecated
{{/isDeprecated}}
{{#externalDocs}}
* {{description}}
* @see <a href="{{url}}">{{summary}} Documentation</a>
{{/externalDocs}}
*/
{{#isDeprecated}}
@Deprecated
{{/isDeprecated}}
public com.squareup.okhttp.Call {{operationId}}Call({{#allParams}}{{{dataType}}} {{paramName}}, {{/allParams}}final ProgressResponseBody.ProgressListener progressListener, final ProgressRequestBody.ProgressRequestListener progressRequestListener) throws ApiException {
Object {{localVariablePrefix}}localVarPostBody = {{#bodyParam}}{{paramName}}{{/bodyParam}}{{^bodyParam}}null{{/bodyParam}};
// create path and map variables
String {{localVariablePrefix}}localVarPath = "{{{path}}}"{{#pathParams}}
.replaceAll("\\{" + "{{baseName}}" + "\\}", {{localVariablePrefix}}apiClient.escapeString({{{paramName}}}.toString())){{/pathParams}};
{{javaUtilPrefix}}List<Pair> {{localVariablePrefix}}localVarQueryParams = new {{javaUtilPrefix}}ArrayList<Pair>();
{{javaUtilPrefix}}List<Pair> {{localVariablePrefix}}localVarCollectionQueryParams = new {{javaUtilPrefix}}ArrayList<Pair>();{{#queryParams}}
if ({{paramName}} != null)
{{localVariablePrefix}}{{#collectionFormat}}localVarCollectionQueryParams.addAll({{localVariablePrefix}}apiClient.parameterToPairs("{{{collectionFormat}}}", {{/collectionFormat}}{{^collectionFormat}}localVarQueryParams.addAll({{localVariablePrefix}}apiClient.parameterToPair({{/collectionFormat}}"{{baseName}}", {{paramName}}));{{/queryParams}}
{{javaUtilPrefix}}Map<String, String> {{localVariablePrefix}}localVarHeaderParams = new {{javaUtilPrefix}}HashMap<String, String>();{{#headerParams}}
if ({{paramName}} != null)
{{localVariablePrefix}}localVarHeaderParams.put("{{baseName}}", {{localVariablePrefix}}apiClient.parameterToString({{paramName}}));{{/headerParams}}
{{javaUtilPrefix}}Map<String, Object> {{localVariablePrefix}}localVarFormParams = new {{javaUtilPrefix}}HashMap<String, Object>();{{#formParams}}
if ({{paramName}} != null)
{{localVariablePrefix}}localVarFormParams.put("{{baseName}}", {{paramName}});{{/formParams}}
final String[] {{localVariablePrefix}}localVarAccepts = {
{{#produces}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/produces}}
};
final String {{localVariablePrefix}}localVarAccept = {{localVariablePrefix}}apiClient.selectHeaderAccept({{localVariablePrefix}}localVarAccepts);
if ({{localVariablePrefix}}localVarAccept != null) {{localVariablePrefix}}localVarHeaderParams.put("Accept", {{localVariablePrefix}}localVarAccept);
final String[] {{localVariablePrefix}}localVarContentTypes = {
{{#consumes}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/consumes}}
};
final String {{localVariablePrefix}}localVarContentType = {{localVariablePrefix}}apiClient.selectHeaderContentType({{localVariablePrefix}}localVarContentTypes);
{{localVariablePrefix}}localVarHeaderParams.put("Content-Type", {{localVariablePrefix}}localVarContentType);
if(progressListener != null) {
apiClient.getHttpClient().networkInterceptors().add(new com.squareup.okhttp.Interceptor() {
@Override
public com.squareup.okhttp.Response intercept(com.squareup.okhttp.Interceptor.Chain chain) throws IOException {
com.squareup.okhttp.Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new ProgressResponseBody(originalResponse.body(), progressListener))
.build();
}
});
}
String[] {{localVariablePrefix}}localVarAuthNames = new String[] { {{#authMethods}}"{{name}}"{{#hasMore}}, {{/hasMore}}{{/authMethods}} };
return {{localVariablePrefix}}apiClient.buildCall({{localVariablePrefix}}localVarPath, "{{httpMethod}}", {{localVariablePrefix}}localVarQueryParams, {{localVariablePrefix}}localVarCollectionQueryParams, {{localVariablePrefix}}localVarPostBody, {{localVariablePrefix}}localVarHeaderParams, {{localVariablePrefix}}localVarFormParams, {{localVariablePrefix}}localVarAuthNames, progressRequestListener);
}
{{#isDeprecated}}
@Deprecated
{{/isDeprecated}}
@SuppressWarnings("rawtypes")
private com.squareup.okhttp.Call {{operationId}}ValidateBeforeCall({{#allParams}}{{{dataType}}} {{paramName}}, {{/allParams}}final ProgressResponseBody.ProgressListener progressListener, final ProgressRequestBody.ProgressRequestListener progressRequestListener) throws ApiException {
{{^performBeanValidation}}
{{#allParams}}{{#required}}
// verify the required parameter '{{paramName}}' is set
if ({{paramName}} == null) {
throw new ApiException("Missing the required parameter '{{paramName}}' when calling {{operationId}}(Async)");
}
{{/required}}{{/allParams}}
com.squareup.okhttp.Call {{localVariablePrefix}}call = {{operationId}}Call({{#allParams}}{{paramName}}, {{/allParams}}progressListener, progressRequestListener);
return {{localVariablePrefix}}call;
{{/performBeanValidation}}
{{#performBeanValidation}}
try {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
ExecutableValidator executableValidator = factory.getValidator().forExecutables();
Object[] parameterValues = { {{#allParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/allParams}} };
Method method = this.getClass().getMethod("{{operationId}}WithHttpInfo"{{#allParams}}, {{#isListContainer}}java.util.List{{/isListContainer}}{{#isMapContainer}}java.util.Map{{/isMapContainer}}{{^isListContainer}}{{^isMapContainer}}{{{dataType}}}{{/isMapContainer}}{{/isListContainer}}.class{{/allParams}});
Set<ConstraintViolation<{{classname}}>> violations = executableValidator.validateParameters(this, method,
parameterValues);
if (violations.size() == 0) {
com.squareup.okhttp.Call {{localVariablePrefix}}call = {{operationId}}Call({{#allParams}}{{paramName}}, {{/allParams}}progressListener, progressRequestListener);
return {{localVariablePrefix}}call;
} else {
throw new BeanValidationException((Set) violations);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
throw new ApiException(e.getMessage());
} catch (SecurityException e) {
e.printStackTrace();
throw new ApiException(e.getMessage());
}
{{/performBeanValidation}}
}
/**
* {{summary}}
* {{notes}}{{#allParams}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{/allParams}}{{#returnType}}
* @return {{returnType}}{{/returnType}}
* @throws ApiException If fail to call the API, e.g. server error or cannot deserialize the response body
{{#isDeprecated}}
* @deprecated
{{/isDeprecated}}
{{#externalDocs}}
* {{description}}
* @see <a href="{{url}}">{{summary}} Documentation</a>
{{/externalDocs}}
*/
{{#isDeprecated}}
@Deprecated
{{/isDeprecated}}
public {{#returnType}}{{{returnType}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) throws ApiException {
{{#returnType}}ApiResponse<{{{returnType}}}> {{localVariablePrefix}}resp = {{/returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/allParams}});{{#returnType}}
return {{localVariablePrefix}}resp.getData();{{/returnType}}
}
/**
* {{summary}}
* {{notes}}{{#allParams}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{/allParams}}
* @return ApiResponse&lt;{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}Void{{/returnType}}&gt;
* @throws ApiException If fail to call the API, e.g. server error or cannot deserialize the response body
{{#isDeprecated}}
* @deprecated
{{/isDeprecated}}
{{#externalDocs}}
* {{description}}
* @see <a href="{{url}}">{{summary}} Documentation</a>
{{/externalDocs}}
*/
{{#isDeprecated}}
@Deprecated
{{/isDeprecated}}
public ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{operationId}}WithHttpInfo({{#allParams}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{{dataType}}} {{paramName}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) throws ApiException {
com.squareup.okhttp.Call {{localVariablePrefix}}call = {{operationId}}ValidateBeforeCall({{#allParams}}{{paramName}}, {{/allParams}}null, null);
{{#returnType}}Type {{localVariablePrefix}}localVarReturnType = new TypeToken<{{{returnType}}}>(){}.getType();
return {{localVariablePrefix}}apiClient.execute({{localVariablePrefix}}call, {{localVariablePrefix}}localVarReturnType);{{/returnType}}{{^returnType}}return {{localVariablePrefix}}apiClient.execute({{localVariablePrefix}}call);{{/returnType}}
}
/**
* {{summary}} (asynchronously)
* {{notes}}{{#allParams}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{/allParams}}
* @param callback The callback to be executed when the API call finishes
* @return The request call
* @throws ApiException If fail to process the API call, e.g. serializing the request body object
{{#isDeprecated}}
* @deprecated
{{/isDeprecated}}
{{#externalDocs}}
* {{description}}
* @see <a href="{{url}}">{{summary}} Documentation</a>
{{/externalDocs}}
*/
{{#isDeprecated}}
@Deprecated
{{/isDeprecated}}
public com.squareup.okhttp.Call {{operationId}}Async({{#allParams}}{{{dataType}}} {{paramName}}, {{/allParams}}final ApiCallback<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{localVariablePrefix}}callback) throws ApiException {
ProgressResponseBody.ProgressListener progressListener = null;
ProgressRequestBody.ProgressRequestListener progressRequestListener = null;
if (callback != null) {
progressListener = new ProgressResponseBody.ProgressListener() {
@Override
public void update(long bytesRead, long contentLength, boolean done) {
callback.onDownloadProgress(bytesRead, contentLength, done);
}
};
progressRequestListener = new ProgressRequestBody.ProgressRequestListener() {
@Override
public void onRequestProgress(long bytesWritten, long contentLength, boolean done) {
callback.onUploadProgress(bytesWritten, contentLength, done);
}
};
}
com.squareup.okhttp.Call {{localVariablePrefix}}call = {{operationId}}ValidateBeforeCall({{#allParams}}{{paramName}}, {{/allParams}}progressListener, progressRequestListener);
{{#returnType}}Type {{localVariablePrefix}}localVarReturnType = new TypeToken<{{{returnType}}}>(){}.getType();
{{localVariablePrefix}}apiClient.executeAsync({{localVariablePrefix}}call, {{localVariablePrefix}}localVarReturnType, {{localVariablePrefix}}callback);{{/returnType}}{{^returnType}}{{localVariablePrefix}}apiClient.executeAsync({{localVariablePrefix}}call, {{localVariablePrefix}}callback);{{/returnType}}
return {{localVariablePrefix}}call;
}
{{/operation}}
public static class Builder {
private AWSAuthenticationCredentials awsAuthenticationCredentials;
private LWAAuthorizationCredentials lwaAuthorizationCredentials;
private String endpoint;
public Builder awsAuthenticationCredentials(AWSAuthenticationCredentials awsAuthenticationCredentials) {
this.awsAuthenticationCredentials = awsAuthenticationCredentials;
return this;
}
public Builder lwaAuthorizationCredentials(LWAAuthorizationCredentials lwaAuthorizationCredentials) {
this.lwaAuthorizationCredentials = lwaAuthorizationCredentials;
return this;
}
public Builder endpoint(String endpoint) {
this.endpoint = endpoint;
return this;
}
public {{classname}} build() {
if (awsAuthenticationCredentials == null) {
throw new RuntimeException("AWSAuthenticationCredentials not set");
}
if (lwaAuthorizationCredentials == null) {
throw new RuntimeException("LWAAuthorizationCredentials not set");
}
if (StringUtil.isEmpty(endpoint)) {
throw new RuntimeException("Endpoint not set");
}
AWSSigV4Signer awsSigV4Signer = new AWSSigV4Signer(awsAuthenticationCredentials);
LWAAuthorizationSigner lwaAuthorizationSigner = new LWAAuthorizationSigner(lwaAuthorizationCredentials);
return new {{classname}}(new ApiClient()
.setAWSSigV4Signer(awsSigV4Signer)
.setLWAAuthorizationSigner(lwaAuthorizationSigner)
.setBasePath(endpoint));
}
}
}
{{/operations}}

View File

@ -0,0 +1,30 @@
package com.amazon.SellingPartnerAPIAA;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
/**
* AWSAuthenticationCredentials
*/
@Data
@Builder
public class AWSAuthenticationCredentials {
/**
* AWS IAM User Access Key Id
*/
@NonNull
private String accessKeyId;
/**
* AWS IAM User Secret Key
*/
@NonNull
private String secretKey;
/**
* AWS Region
*/
@NonNull
private String region;
}

View File

@ -0,0 +1,48 @@
package com.amazon.SellingPartnerAPIAA;
import com.amazonaws.SignableRequest;
import com.amazonaws.auth.AWS4Signer;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.BasicAWSCredentials;
import com.squareup.okhttp.Request;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
/**
* AWS Signature Version 4 Signer
*/
public class AWSSigV4Signer {
private static final String SERVICE_NAME = "execute-api";
@Setter(AccessLevel.PACKAGE)
@Getter(AccessLevel.PACKAGE)
private AWS4Signer aws4Signer;
private AWSCredentials awsCredentials;
/**
*
* @param awsAuthenticationCredentials AWS Developer Account Credentials
*/
public AWSSigV4Signer(AWSAuthenticationCredentials awsAuthenticationCredentials) {
aws4Signer = new AWS4Signer();
aws4Signer.setServiceName(SERVICE_NAME);
aws4Signer.setRegionName(awsAuthenticationCredentials.getRegion());
awsCredentials = new BasicAWSCredentials(awsAuthenticationCredentials.getAccessKeyId(),
awsAuthenticationCredentials.getSecretKey());
}
/**
* Signs a Request with AWS Signature Version 4
*
* @param originalRequest Request to sign (treated as immutable)
* @return Copy of originalRequest with AWS Signature
*/
public Request sign(Request originalRequest) {
SignableRequest<Request> signableRequest = new SignableRequestImpl(originalRequest);
aws4Signer.sign(signableRequest, awsCredentials);
return (Request) signableRequest.getOriginalRequestObject();
}
}

View File

@ -0,0 +1,25 @@
package com.amazon.SellingPartnerAPIAA;
import com.google.gson.annotations.SerializedName;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
class LWAAccessTokenRequestMeta {
@SerializedName("grant_type")
private String grantType;
@SerializedName("refresh_token")
private String refreshToken;
@SerializedName("client_id")
private String clientId;
@SerializedName("client_secret")
private String clientSecret;
@SerializedName("scope")
private LWAClientScopes scopes;
}

View File

@ -0,0 +1,65 @@
package com.amazon.SellingPartnerAPIAA;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import java.util.Arrays;
import java.util.HashSet;
/**
* LWAAuthorizationCredentials
*/
@Data
@Builder
public class LWAAuthorizationCredentials {
/**
* LWA Client Id
*/
@NonNull
private String clientId;
/**
* LWA Client Secret
*/
@NonNull
private String clientSecret;
/**
* LWA Refresh Token
*/
private String refreshToken;
/**
* LWA Authorization Server Endpoint
*/
@NonNull
private String endpoint;
/**
* LWA Client Scopes
*/
private LWAClientScopes scopes;
public static class LWAAuthorizationCredentialsBuilder {
{
scopes = new LWAClientScopes(new HashSet<>());
}
public LWAAuthorizationCredentialsBuilder withScope(String scope) {
return withScopes(scope);
}
public LWAAuthorizationCredentialsBuilder withScopes(String... scopes) {
if (scopes != null) {
Arrays.stream(scopes)
.forEach(this.scopes::addScope);
}
return this;
}
}
}

View File

@ -0,0 +1,55 @@
package com.amazon.SellingPartnerAPIAA;
import com.squareup.okhttp.Request;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
/**
* LWA Authorization Signer
*/
public class LWAAuthorizationSigner {
private static final String SIGNED_ACCESS_TOKEN_HEADER_NAME = "x-amz-access-token";
private final String tokenRequestGrantType;
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private LWAClient lwaClient;
private LWAAccessTokenRequestMeta lwaAccessTokenRequestMeta;
/**
*
* @param lwaAuthorizationCredentials LWA Authorization Credentials for token exchange
*/
public LWAAuthorizationSigner(LWAAuthorizationCredentials lwaAuthorizationCredentials) {
lwaClient = new LWAClient(lwaAuthorizationCredentials.getEndpoint());
if (!lwaAuthorizationCredentials.getScopes().isEmpty()) {
tokenRequestGrantType = "client_credentials";
} else {
tokenRequestGrantType = "refresh_token";
}
lwaAccessTokenRequestMeta = LWAAccessTokenRequestMeta.builder()
.clientId(lwaAuthorizationCredentials.getClientId())
.clientSecret(lwaAuthorizationCredentials.getClientSecret())
.refreshToken(lwaAuthorizationCredentials.getRefreshToken())
.grantType(tokenRequestGrantType).scopes(lwaAuthorizationCredentials.getScopes())
.build();
}
/**
* Signs a Request with an LWA Access Token
* @param originalRequest Request to sign (treated as immutable)
* @return Copy of originalRequest with LWA signature
*/
public Request sign(Request originalRequest) {
String accessToken = lwaClient.getAccessToken(lwaAccessTokenRequestMeta);
return originalRequest.newBuilder()
.addHeader(SIGNED_ACCESS_TOKEN_HEADER_NAME, accessToken)
.build();
}
}

View File

@ -0,0 +1,58 @@
package com.amazon.SellingPartnerAPIAA;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import java.io.IOException;
class LWAClient {
private static final String ACCESS_TOKEN_KEY = "access_token";
private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
@Getter
private String endpoint;
@Setter(AccessLevel.PACKAGE)
private OkHttpClient okHttpClient;
LWAClient(String endpoint) {
okHttpClient = new OkHttpClient();
this.endpoint = endpoint;
}
String getAccessToken(LWAAccessTokenRequestMeta lwaAccessTokenRequestMeta) {
RequestBody requestBody = RequestBody.create(JSON_MEDIA_TYPE,
new Gson().toJson(lwaAccessTokenRequestMeta));
Request accessTokenRequest = new Request.Builder()
.url(endpoint)
.post(requestBody)
.build();
String accessToken;
try {
Response response = okHttpClient.newCall(accessTokenRequest).execute();
if (!response.isSuccessful()) {
throw new IOException("Unsuccessful LWA token exchange");
}
JsonObject responseJson = new JsonParser()
.parse(response.body().string())
.getAsJsonObject();
accessToken = responseJson.get(ACCESS_TOKEN_KEY).getAsString();
} catch (Exception e) {
throw new RuntimeException("Error getting LWA Access Token", e);
}
return accessToken;
}
}

View File

@ -0,0 +1,27 @@
package com.amazon.SellingPartnerAPIAA;
import com.google.gson.annotations.JsonAdapter;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Set;
@AllArgsConstructor
@Getter
@JsonAdapter(LWAClientScopesSerializerDeserializer.class)
public class LWAClientScopes {
private final Set<String> scopes;
protected void addScope(String scope) {
scopes.add(scope);
}
protected boolean isEmpty() {
return scopes.isEmpty();
}
}

View File

@ -0,0 +1,34 @@
package com.amazon.SellingPartnerAPIAA;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class LWAClientScopesSerializerDeserializer implements JsonDeserializer<LWAClientScopes>,
JsonSerializer<LWAClientScopes> {
@Override
public JsonElement serialize(LWAClientScopes src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(String.join(" ", src.getScopes()));
}
@Override
public LWAClientScopes deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext)
throws JsonParseException {
JsonObject jsonObj = jsonElement.getAsJsonObject();
Set<String> scopeSet = new HashSet<>(Arrays.asList(jsonObj.get("scope").getAsString().split(" ")));
return new LWAClientScopes(scopeSet);
}
}

View File

@ -0,0 +1,11 @@
package com.amazon.SellingPartnerAPIAA;
public final class ScopeConstants {
private ScopeConstants(){
}
public static final String SCOPE_NOTIFICATIONS_API = "sellingpartnerapi::notifications";
public static final String SCOPE_MIGRATION_API = "sellingpartnerapi::migration";
}

View File

@ -0,0 +1,153 @@
package com.amazon.SellingPartnerAPIAA;
import com.amazonaws.ReadLimitInfo;
import com.amazonaws.SignableRequest;
import com.amazonaws.http.HttpMethodName;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.Request;
import okio.Buffer;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class SignableRequestImpl implements SignableRequest<Request> {
private static final String CONTENT_TYPE_HEADER_NAME = "Content-Type";
private Request originalRequest;
private Request.Builder signableRequestBuilder;
SignableRequestImpl(Request originalRequest) {
this.originalRequest = originalRequest;
signableRequestBuilder = originalRequest.newBuilder();
}
@Override
public void addHeader(String name, String value) {
signableRequestBuilder.addHeader(name, value);
}
@Override
public void addParameter(String name, String value) {
HttpUrl newUrl = signableRequestBuilder.build()
.httpUrl()
.newBuilder()
.addEncodedQueryParameter(name, value)
.build();
signableRequestBuilder.url(newUrl);
}
@Override
public void setContent(InputStream inputStream) {
throw new UnsupportedOperationException();
}
@Override
public Map<String, String> getHeaders() {
Map<String, String> headers = new HashMap<>();
Request requestSnapshot = signableRequestBuilder.build();
requestSnapshot.headers()
.names()
.forEach(headerName -> headers.put(headerName, requestSnapshot.header(headerName)));
if (requestSnapshot.body() != null) {
MediaType contentType = requestSnapshot.body().contentType();
if (contentType != null) {
headers.put(CONTENT_TYPE_HEADER_NAME, contentType.toString());
}
}
return headers;
}
@Override
public String getResourcePath() {
try {
return originalRequest.url()
.toURI()
.getPath();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
@Override
public Map<String, List<String>> getParameters() {
Map<String, List<String>> parameters = new HashMap<>();
try {
List<NameValuePair> nameValuePairs = URLEncodedUtils.parse(originalRequest.url().toURI(),
StandardCharsets.UTF_8);
nameValuePairs.forEach(nameValuePair -> parameters.put(nameValuePair.getName(),
Collections.singletonList(nameValuePair.getValue())));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
return parameters;
}
@Override
public URI getEndpoint() {
URI uri = null;
try {
uri = originalRequest.url().toURI();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
return URI.create(String.format("%s://%s", uri.getScheme(), uri.getHost()));
}
@Override
public HttpMethodName getHttpMethod() {
return HttpMethodName.fromValue(originalRequest.method().toUpperCase());
}
@Override
public int getTimeOffset() {
return 0;
}
@Override
public InputStream getContent() {
ByteArrayInputStream inputStream = null;
if (originalRequest.body() != null) {
try {
Buffer buffer = new Buffer();
originalRequest.body().writeTo(buffer);
inputStream = new ByteArrayInputStream(buffer.readByteArray());
} catch (IOException e) {
throw new RuntimeException("Unable to buffer request body", e);
}
}
return inputStream;
}
@Override
public InputStream getContentUnwrapped() {
return getContent();
}
@Override
public ReadLimitInfo getReadLimitInfo() {
return null;
}
@Override
public Object getOriginalRequestObject() {
return signableRequestBuilder.build();
}
}

View File

@ -0,0 +1,83 @@
package com.amazon.SellingPartnerAPIAA;
import com.amazonaws.SignableRequest;
import com.amazonaws.auth.AWS4Signer;
import com.amazonaws.auth.AWSCredentials;
import com.squareup.okhttp.Request;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
@RunWith(MockitoJUnitRunner.class)
public class AWSSigV4SignerTest {
private static final String TEST_ACCESS_KEY_ID = "aKey";
private static final String TEST_SECRET_KEY = "sKey";
private static final String TEST_REGION = "us-east";
@Mock
private AWS4Signer mockAWS4Signer;
private AWSSigV4Signer underTest;
@Before
public void init() {
underTest = new AWSSigV4Signer(AWSAuthenticationCredentials.builder()
.accessKeyId(TEST_ACCESS_KEY_ID)
.secretKey(TEST_SECRET_KEY)
.region(TEST_REGION)
.build()
);
}
@Test
public void signRequestUsingProvidedCredentials() {
ArgumentCaptor<AWSCredentials> awsCredentialsArgumentCaptor = ArgumentCaptor.forClass(AWSCredentials.class);
underTest.setAws4Signer(mockAWS4Signer);
doNothing()
.when(mockAWS4Signer)
.sign(any(SignableRequest.class), awsCredentialsArgumentCaptor.capture());
underTest.sign(new Request.Builder().url("https://api.amazon.com").build());
AWSCredentials actualAWSCredentials = awsCredentialsArgumentCaptor.getValue();
assertEquals(TEST_ACCESS_KEY_ID, actualAWSCredentials.getAWSAccessKeyId());
assertEquals(TEST_SECRET_KEY, actualAWSCredentials.getAWSSecretKey());
}
@Test
public void setSignerRegion() {
assertEquals(TEST_REGION, underTest.getAws4Signer().getRegionName());
}
@Test
public void setSignerServiceName() {
assertEquals("execute-api", underTest.getAws4Signer().getServiceName());
}
@Test
public void returnSignedRequest() {
ArgumentCaptor<SignableRequest> signableRequestArgumentCaptor = ArgumentCaptor.forClass(SignableRequest.class);
underTest.setAws4Signer(mockAWS4Signer);
Request actualSignedRequest = underTest.sign(new Request.Builder()
.url("http://api.amazon.com")
.build());
verify(mockAWS4Signer)
.sign(signableRequestArgumentCaptor.capture(), any(AWSCredentials.class));
SignableRequest actualSignableRequest = signableRequestArgumentCaptor.getValue();
assertEquals(((Request)actualSignableRequest.getOriginalRequestObject()).url(), actualSignedRequest.url());
}
}

View File

@ -0,0 +1,131 @@
package com.amazon.SellingPartnerAPIAA;
import com.squareup.okhttp.Request;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import java.util.Arrays;
import java.util.HashSet;
import java.util.stream.Stream;
import static com.amazon.SellingPartnerAPIAA.ScopeConstants.SCOPE_MIGRATION_API;
import static com.amazon.SellingPartnerAPIAA.ScopeConstants.SCOPE_NOTIFICATIONS_API;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class LWAAuthorizationSignerTest {
private static final String TEST_REFRESH_TOKEN = "rToken";
private static final String TEST_CLIENT_SECRET = "cSecret";
private static final String TEST_CLIENT_ID = "cId";
private static final String TEST_ENDPOINT = "https://www.amazon.com/lwa";
private static final String TEST_SCOPE_1 = SCOPE_NOTIFICATIONS_API;
private static final String TEST_SCOPE_2 = SCOPE_MIGRATION_API;
private static final String SELLER_TYPE_SELLER = "seller";
private static final String SELLER_TYPE_SELLERLESS = "sellerless";
private Request request;
private static LWAAuthorizationSigner underTestSeller;
private static LWAAuthorizationSigner underTestSellerless;
static {
underTestSeller = new LWAAuthorizationSigner(LWAAuthorizationCredentials.builder()
.clientId(TEST_CLIENT_ID)
.clientSecret(TEST_CLIENT_SECRET)
.refreshToken(TEST_REFRESH_TOKEN)
.endpoint(TEST_ENDPOINT)
.build());
underTestSellerless = new LWAAuthorizationSigner(LWAAuthorizationCredentials.builder()
.clientId(TEST_CLIENT_ID)
.clientSecret(TEST_CLIENT_SECRET)
.withScopes(TEST_SCOPE_1, TEST_SCOPE_2)
.endpoint(TEST_ENDPOINT)
.build());
}
@BeforeEach
public void init() {
request = new Request.Builder()
.url("https://www.amazon.com/api")
.build();
}
public static Stream<Arguments> lwaAuthSigner(){
return Stream.of(
Arguments.of(SELLER_TYPE_SELLER, underTestSeller),
Arguments.of(SELLER_TYPE_SELLERLESS, underTestSellerless)
);
}
@ParameterizedTest
@MethodSource("lwaAuthSigner")
public void initializeLWAClientWithConfiguredEndpoint(String sellerType, LWAAuthorizationSigner testAuthSigner) {
assertEquals(TEST_ENDPOINT, testAuthSigner.getLwaClient().getEndpoint());
}
@ParameterizedTest
@MethodSource("lwaAuthSigner")
public void requestLWAAccessTokenFromConfiguration(String sellerType, LWAAuthorizationSigner testAuthSigner) {
LWAClient mockLWAClient = mock(LWAClient.class);
ArgumentCaptor<LWAAccessTokenRequestMeta> lwaAccessTokenRequestMetaArgumentCaptor = ArgumentCaptor.forClass(
LWAAccessTokenRequestMeta.class);
when(mockLWAClient.getAccessToken(lwaAccessTokenRequestMetaArgumentCaptor.capture()))
.thenReturn("foo");
testAuthSigner.setLwaClient(mockLWAClient);
testAuthSigner.sign(request);
LWAAccessTokenRequestMeta actualLWAAccessTokenRequestMeta = lwaAccessTokenRequestMetaArgumentCaptor.getValue();
assertEquals(TEST_REFRESH_TOKEN, actualLWAAccessTokenRequestMeta.getRefreshToken());
assertEquals(TEST_CLIENT_SECRET, actualLWAAccessTokenRequestMeta.getClientSecret());
assertEquals(TEST_CLIENT_ID, actualLWAAccessTokenRequestMeta.getClientId());
if(sellerType.equals(SELLER_TYPE_SELLER)){
Assert.assertTrue(actualLWAAccessTokenRequestMeta.getScopes().getScopes().isEmpty());
assertEquals("refresh_token", actualLWAAccessTokenRequestMeta.getGrantType());
}
else if (sellerType.equals(SELLER_TYPE_SELLERLESS)){
assertEquals(new HashSet<String>(Arrays.asList(TEST_SCOPE_1, TEST_SCOPE_2)), actualLWAAccessTokenRequestMeta.getScopes().getScopes());
assertEquals("client_credentials", actualLWAAccessTokenRequestMeta.getGrantType());
}
}
@ParameterizedTest
@MethodSource("lwaAuthSigner")
public void returnSignedRequestWithAccessTokenFromLWAClient(String sellerType, LWAAuthorizationSigner testAuthSigner) {
LWAClient mockLWAClient = mock(LWAClient.class);
when(mockLWAClient.getAccessToken(any(LWAAccessTokenRequestMeta.class)))
.thenReturn("Azta|Foo");
testAuthSigner.setLwaClient(mockLWAClient);
Request actualSignedRequest = testAuthSigner.sign(request);
assertEquals("Azta|Foo", actualSignedRequest.header("x-amz-access-token"));
}
@ParameterizedTest
@MethodSource("lwaAuthSigner")
public void originalRequestIsImmutable(String sellerType, LWAAuthorizationSigner testAuthSigner) {
LWAClient mockLWAClient = mock(LWAClient.class);
when(mockLWAClient.getAccessToken(any(LWAAccessTokenRequestMeta.class)))
.thenReturn("Azta|Foo");
testAuthSigner.setLwaClient(mockLWAClient);
assertNotSame(request, testAuthSigner.sign(request));
}
}

View File

@ -0,0 +1,78 @@
package com.amazon.SellingPartnerAPIAA;
import com.google.gson.Gson;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;
import static com.amazon.SellingPartnerAPIAA.ScopeConstants.SCOPE_MIGRATION_API;
import static com.amazon.SellingPartnerAPIAA.ScopeConstants.SCOPE_NOTIFICATIONS_API;
public class LWAClientScopesSerializerDeserializerTest {
private static final String TEST_SCOPE_1 = SCOPE_NOTIFICATIONS_API;
private static final String TEST_SCOPE_2 = SCOPE_MIGRATION_API;
private static final Set<String> scopesTestSellerless = new HashSet<String>(Arrays.asList(TEST_SCOPE_1,
TEST_SCOPE_2));
private static final String SELLER_TYPE_SELLER = "seller";
private static final String SELLER_TYPE_SELLERLESS = "sellerless";
private Gson gson;
@BeforeEach
public void setup() {
gson = new Gson();
}
public static Stream<Arguments> scopeSerialization(){
return Stream.of(
Arguments.of(SELLER_TYPE_SELLER, null),
Arguments.of(SELLER_TYPE_SELLERLESS, new LWAClientScopes(scopesTestSellerless))
);
}
public static Stream<Arguments> scopeDeserialization(){
return Stream.of(
Arguments.of(SELLER_TYPE_SELLER, null),
Arguments.of(SELLER_TYPE_SELLERLESS, "{\"scope\":\"sellingpartnerapi::migration sellingpartnerapi::notifications\"}")
);
}
@ParameterizedTest
@MethodSource("scopeSerialization")
public void testSerializeScope(String sellerType, LWAClientScopes testScope){
String scopeJSON = gson.toJson(testScope);
if (sellerType.equals(SELLER_TYPE_SELLER)) {
Assert.assertEquals("null", scopeJSON);
}
else if (sellerType.equals(SELLER_TYPE_SELLERLESS)){
Assert.assertTrue(!scopeJSON.isEmpty());
}
}
@ParameterizedTest
@MethodSource("scopeDeserialization")
public void testDeserializeScope(String sellerType, String serializedValue){
LWAClientScopes deserializedValue = gson.fromJson(serializedValue, LWAClientScopes.class);
if (sellerType.equals(SELLER_TYPE_SELLER)) {
Assert.assertNull(deserializedValue);
}
else if (sellerType.equals(SELLER_TYPE_SELLERLESS)){
Assert.assertEquals(deserializedValue.getScopes(),scopesTestSellerless);
}
}
}

View File

@ -0,0 +1,185 @@
package com.amazon.SellingPartnerAPIAA;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.squareup.okhttp.Call;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import okio.Buffer;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;
import static com.amazon.SellingPartnerAPIAA.ScopeConstants.SCOPE_MIGRATION_API;
import static com.amazon.SellingPartnerAPIAA.ScopeConstants.SCOPE_NOTIFICATIONS_API;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class LWAClientTest {
private static final String TEST_ENDPOINT = "https://www.amazon.com/api";
private static final MediaType EXPECTED_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
private static LWAAccessTokenRequestMeta lwaAccessTokenRequestMetaSeller;
private static LWAAccessTokenRequestMeta lwaAccessTokenRequestMetaSellerless;
private static final String TEST_SCOPE_1 = SCOPE_NOTIFICATIONS_API;
private static final String TEST_SCOPE_2 = SCOPE_MIGRATION_API;
private static final Set<String> scopesTestSellerless = new HashSet<String>(Arrays.asList(TEST_SCOPE_1,
TEST_SCOPE_2));
private static final String SELLER_TYPE_SELLER = "seller";
private static final String SELLER_TYPE_SELLERLESS = "sellerless";
@Mock
private OkHttpClient mockOkHttpClient;
@Mock
private Call mockCall;
private LWAClient underTest;
static {
lwaAccessTokenRequestMetaSeller = LWAAccessTokenRequestMeta.builder()
.refreshToken("rToken")
.clientId("cId")
.clientSecret("cSecret")
.grantType("rToken")
.build();
lwaAccessTokenRequestMetaSellerless = LWAAccessTokenRequestMeta.builder()
.clientId("cId")
.clientSecret("cSecret")
.grantType("cCredentials")
.scopes(new LWAClientScopes(scopesTestSellerless))
.build();
}
@Before
@BeforeEach
public void init() {
MockitoAnnotations.initMocks(this);
underTest = new LWAClient(TEST_ENDPOINT);
underTest.setOkHttpClient(mockOkHttpClient);
}
public static Stream<Arguments> lwaClient(){
return Stream.of(
Arguments.of(SELLER_TYPE_SELLER, lwaAccessTokenRequestMetaSeller),
Arguments.of(SELLER_TYPE_SELLERLESS, lwaAccessTokenRequestMetaSellerless)
);
}
@Test
public void initializeEndpoint() {
assertEquals(TEST_ENDPOINT, underTest.getEndpoint());
}
@ParameterizedTest
@MethodSource("lwaClient")
public void makeRequestFromMeta (String sellerType, LWAAccessTokenRequestMeta testLwaAccessTokenRequestMeta) throws IOException {
ArgumentCaptor<Request> requestArgumentCaptor = ArgumentCaptor.forClass(Request.class);
when(mockOkHttpClient.newCall(requestArgumentCaptor.capture()))
.thenReturn(mockCall);
when(mockCall.execute())
.thenReturn(buildResponse(200, "foo"));
underTest.getAccessToken(testLwaAccessTokenRequestMeta);
Request actualRequest = requestArgumentCaptor.getValue();
assertEquals(TEST_ENDPOINT, actualRequest.url().toString());
assertEquals("POST", actualRequest.method().toUpperCase());
Buffer bodyBuffer = new Buffer();
actualRequest.body().writeTo(bodyBuffer);
JsonObject bodyJson = new JsonParser()
.parse(new InputStreamReader(bodyBuffer.inputStream()))
.getAsJsonObject();
if (sellerType == SELLER_TYPE_SELLER) {
assertEquals("rToken", bodyJson.get("refresh_token")
.getAsString());
}
else if (sellerType == SELLER_TYPE_SELLERLESS){
assertNotNull(bodyJson.get("scope"));
}
assertEquals("cId", bodyJson.get("client_id").getAsString());
assertEquals("cSecret", bodyJson.get("client_secret").getAsString());
assertEquals(EXPECTED_MEDIA_TYPE, actualRequest.body().contentType());
}
@ParameterizedTest
@MethodSource("lwaClient")
public void returnAccessTokenFromResponse(String sellerType, LWAAccessTokenRequestMeta testLwaAccessTokenRequestMeta) throws IOException {
when(mockOkHttpClient.newCall(any(Request.class)))
.thenReturn(mockCall);
when(mockCall.execute())
.thenReturn(buildResponse(200, "Azta|foo"));
assertEquals("Azta|foo", underTest.getAccessToken(testLwaAccessTokenRequestMeta));
}
@ParameterizedTest
@MethodSource("lwaClient")
public void unsuccessfulPostThrowsException(String sellerType, LWAAccessTokenRequestMeta testLwaAccessTokenRequestMeta) throws IOException {
when(mockOkHttpClient.newCall(any(Request.class)))
.thenReturn(mockCall);
when(mockCall.execute())
.thenReturn(buildResponse(400, "Azta|foo"));
Assertions.assertThrows(RuntimeException.class, () -> {
underTest.getAccessToken(testLwaAccessTokenRequestMeta);
});
}
@ParameterizedTest
@MethodSource("lwaClient")
public void missingAccessTokenInResponseThrowsException(String sellerType, LWAAccessTokenRequestMeta testLwaAccessTokenRequestMeta) throws IOException {
when(mockOkHttpClient.newCall(any(Request.class)))
.thenReturn(mockCall);
when(mockCall.execute())
.thenReturn(buildResponse(200, ""));
Assertions.assertThrows(RuntimeException.class, () -> {
underTest.getAccessToken(testLwaAccessTokenRequestMeta);
});
}
private static Response buildResponse(int code, String accessToken) {
ResponseBody responseBody = ResponseBody.create(EXPECTED_MEDIA_TYPE,
String.format("{%s:%s}", "access_token", accessToken));
return new Response.Builder()
.request(new Request.Builder().url(TEST_ENDPOINT).build())
.code(code)
.body(responseBody)
.protocol(Protocol.HTTP_1_1)
.message("OK")
.build();
}
}

View File

@ -0,0 +1,236 @@
package com.amazon.SellingPartnerAPIAA;
import com.amazonaws.http.HttpMethodName;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class SignableRequestImplTest {
private Request testRequest;
private SignableRequestImpl underTest;
@Before
public void init() {
testRequest = new Request.Builder()
.url("http://www.amazon.com/request/library?test=true&sky=blue&right=右")
.get()
.build();
underTest = new SignableRequestImpl(testRequest);
}
@Test
public void getHttpMethod() {
assertEquals(HttpMethodName.GET, underTest.getHttpMethod());
underTest = new SignableRequestImpl(new Request.Builder()
.url("https://www.amazon.com")
.post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), "{\"foo\": \"bar\"}"))
.build());
assertEquals(HttpMethodName.POST, underTest.getHttpMethod());
}
@Test
public void getOriginalRequestObject() {
Request actualRequest = (Request)underTest.getOriginalRequestObject();
assertNotSame(testRequest, actualRequest);
assertEquals(testRequest.method(), actualRequest.method());
assertEquals(testRequest.url(), actualRequest.url());
assertEquals(testRequest.headers().toMultimap(), actualRequest.headers().toMultimap());
assertEquals(testRequest.body(), actualRequest.body());
}
@Test
public void getReadLimitInfo() {
assertNull(underTest.getReadLimitInfo());
}
@Test
public void getResourcePath() {
assertEquals("/request/library", underTest.getResourcePath());
}
@Test
public void noTimeOffset() {
assertEquals(0, underTest.getTimeOffset());
}
@Test
public void getEndpoint() {
assertEquals(URI.create("http://www.amazon.com"), underTest.getEndpoint());
}
@Test
public void headers() {
Map<String, String> expectedHeaders = new HashMap<>();
assertTrue(underTest.getHeaders().isEmpty());
underTest.addHeader("foo", "bar");
expectedHeaders.put("foo", "bar");
assertEquals(expectedHeaders, underTest.getHeaders());
underTest.addHeader("ban", "bop");
expectedHeaders.put("ban", "bop");
assertEquals(expectedHeaders, underTest.getHeaders());
}
@Test
public void getParameters() {
Map<String, List<String>> expectedParamters = new HashMap<>();
expectedParamters.put("test", Collections.singletonList("true"));
expectedParamters.put("sky", Collections.singletonList("blue"));
expectedParamters.put("right", Collections.singletonList("右"));
assertEquals(expectedParamters, underTest.getParameters());
}
@Test
public void getContent() {
String expectedContent = "{\"foo\":\"bar\"}";
StringBuilder actualContent = new StringBuilder();
underTest = new SignableRequestImpl(new Request.Builder()
.url("https://www.amazon.com")
.post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), expectedContent))
.build());
try(Scanner scanner = new Scanner(underTest.getContent())){
while(scanner.hasNext()) {
actualContent.append(scanner.next());
}
}
assertEquals(expectedContent, actualContent.toString());
}
@Test
public void getUnwrappedContent() {
String expectedContent = "{\"ban\":\"bop\"}";
StringBuilder actualContent = new StringBuilder();
underTest = new SignableRequestImpl(new Request.Builder()
.url("https://www.amazon.com")
.post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), expectedContent))
.build());
try(Scanner scanner = new Scanner(underTest.getContentUnwrapped())){
while(scanner.hasNext()) {
actualContent.append(scanner.next());
}
}
assertEquals(expectedContent, actualContent.toString());
}
@Test(expected = UnsupportedOperationException.class)
public void setContentNotSupported() {
underTest.setContent(new ByteArrayInputStream("abc".getBytes()));
}
@Test
public void addParameter() {
underTest.addParameter("left", "左");
HttpUrl actualHttpUrl = ((Request) underTest.getOriginalRequestObject())
.httpUrl();
assertEquals(Collections.singletonList("true"), actualHttpUrl.queryParameterValues("test"));
assertEquals(Collections.singletonList("blue"), actualHttpUrl.queryParameterValues("sky"));
assertEquals(Collections.singletonList("右"), actualHttpUrl.queryParameterValues("right"));
assertEquals(Collections.singletonList("左"), actualHttpUrl.queryParameterValues("left"));
}
@Test
public void gracefulBlankParametersParse() {
testRequest = new Request.Builder()
.url("http://www.amazon.com/request/library? ")
.get()
.build();
underTest = new SignableRequestImpl(testRequest);
assertTrue(underTest.getParameters().isEmpty());
}
@Test
public void gracefulIncompleteParameterPairsParse() {
testRequest = new Request.Builder()
.url("http://www.amazon.com/request/library?isSigned& =false")
.get()
.build();
Map<String, List<String>> expected = new HashMap<>();
expected.put("isSigned", Collections.singletonList(null));
expected.put(" ", Collections.singletonList("false"));
underTest = new SignableRequestImpl(testRequest);
assertEquals(expected, underTest.getParameters());
}
@Test
public void getHeadersIncludesContentTypeFromRequestBody() {
String expected = "application/json; charset=utf-8";
RequestBody requestBody = RequestBody.create(MediaType.parse(expected),
"{\"foo\":\"bar\"}");
testRequest = new Request.Builder()
.url("http://www.amazon.com")
.post(requestBody)
.header("Content-Type", "THIS SHOULD BE OVERRIDDEN WITH REQUEST BODY CONTENT TYPE")
.build();
underTest = new SignableRequestImpl(testRequest);
assertEquals(expected, underTest.getHeaders().get("Content-Type"));
}
@Test
public void missingRequestBodyDoesNotOverwriteExistingContentTypeHeader() {
String expected = "testContentType";
testRequest = new Request.Builder()
.url("http://www.amazon.com")
.get()
.header("Content-Type", expected)
.build();
underTest = new SignableRequestImpl(testRequest);
assertEquals(expected, underTest.getHeaders().get("Content-Type"));
}
@Test
public void missingRequestBodyContentTypeDoesNotOverwriteExistingContentTypeHeader() {
String expected = "testContentType";
testRequest = new Request.Builder()
.url("http://www.amazon.com")
.post(RequestBody.create(null, "foo"))
.header("Content-Type", expected)
.build();
underTest = new SignableRequestImpl(testRequest);
assertEquals(expected, underTest.getHeaders().get("Content-Type"));
}
}

View File

@ -0,0 +1,11 @@
.gradle/
.idea/
/build/
/buildSrc
/gradle/
/gradlew
/gradlew.bat
/wrapper/
/target
build.gradle.backup
settings.gradle.backup

View File

@ -0,0 +1,116 @@
#Selling Partner API for Feeds, Uploads and Reports Java Helper
This library provides helper classes for use with the Selling Partner APIs for Feeds, Uploads and Reports. It is intended for use with the Selling Partner API Client Libraries generated by [Swagger Codegen](https://swagger.io/tools/swagger-codegen/) using the OkHttp library.
Use this library to encrypt files to be uploaded (Feeds and Uploads), or decrypt files after downloading (Reports)
## Resources
To use this library you will need to integrate it with one or more of the following packages:
##### Selling Partner API Authentication/Authorization Library
This package is required because it handles the Selling Partner API credentials required by the APIs. You can find it on our GitHub Repository [here](https://github.com/amzn/amazon-marketplace-api-sdk/tree/master/clients/sellingpartner-api-aa-java).
##### Selling Partner API for Feeds
The JSON file can be obtained [here](https://github.com/amzn/amazon-marketplace-api-sdk/tree/master/Feeds%20API).
##### Selling Partner API for Uploads
The JSON file can be obtained [here](https://github.com/amzn/amazon-marketplace-api-sdk/tree/master/Uploads%20API).
##### Selling Partner API for Reports
The JSON file can be obtained [here](https://github.com/amzn/amazon-marketplace-api-sdk/tree/master/Reports%20API).
## Building
#### Building the Client Libraries
- Build the client library for each of the API model JSON files using Swagger Codegen. For each file:
*For example:*
java -jar C:\SwaggerToCL\swagger-codegen-cli.jar generate -i C:\SwaggerToCL\Feeds.json -l java -t [path to clients\sellingpartner-apiaa-java directory]/resources/swagger-codegen/templates/ -o C:\SwaggerToCL\Feeds_JavaCL
#### Integrating the AA Library to the Helper via Maven
1. Build the AA Library and add it as a dependency of the SDKs:
Navigate to the **clients/sellingpartner-api-aa-java** folder and run:
mvn package
This generates a folder named "target". In this folder is a JAR file named **sellingpartnerapi-aa-java-1.0-jar-with-dependencies.jar** (or something similar) and all of the required dependencies.
2. Install the JAR file in your local Maven repository:
*For example:*
mvn install:install-file -Dfile=[path to JAR file in "target" folder] -DgroupId=com.amazon.sellingpartnerapi -DartifactId=sellingpartnerapi-aajava -Dversion=1.0 -Dpackaging=jar
Keep track of the *groupId*, *artifactId*, and *version* values (which can also be found near the top of the **pom.xml** file in the **clients/sellingpartner-api-aa-java** folder) because you will use them in the steps to follow.
3. Add a dependency on the AA library in the **pom.xml** of each client library.
*For example:*
```
<dependency>
<groupId>com.amazon.sellingpartnerapi</groupId>
<artifactId>sellingpartnerapi-aa-java</artifactId>
<version>1.0</version>
</dependency>
```
4. Build each client library with:
```
mvn package
```
5. Install the Feeds, Uploads and Reports client libraries in your local Maven repository.
*For example:*
mvn install:install-file -Dfile=[path to JAR file in "target" folder] -DgroupId=com.amazon.sellingpartnerapi -DartifactId=sellingpartnerapi-feeds-java -Dversion=1.0 -Dpackaging=jar
6. Confirm that the *artifactId* of the Feeds, Uploads and Reports libraries in the **pom.xml** of the Helper matches the ones used when installing each library.
*For example:*
```
<dependency>
<groupId>com.amazon.sellingpartnerapi</groupId>
<artifactId>sellingpartnerapi-feeds-java</artifactId>
<version>1.0</version>
</dependency>
```
7. Navigate to the **SellingPartnerApiFeedsUploadsReportsJavaHelper** folder and run:
```
mvn package
```
*The Feeds, Uploads and Reports Java Helper SDK comes with two building options: Maven and Gradle, however, we only detail how to complete the Maven build above.*
## License
Swagger Codegen templates are subject to the [Swagger Codegen License](https://github.com/swagger-api/swagger-codegen#license).
All other work licensed as follows:
Copyright Amazon.com Inc. or its affiliates.
All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this library except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,107 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.+'
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
}
}
plugins {
id 'idea'
id 'eclipse'
id 'java'
id 'maven'
}
group = 'io.swagger'
version = '1.0.0'
repositories {
jcenter()
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
/*sourceSets {
main {
java {
srcDirs = ['src/main/java']
}
resources {
srcDirs = ['src/main/resources']
}
}
test {
java {
srcDirs = ['src/test/java']
}
resources {
srcDirs = ['src/test/resources']
}
}
}*/
install {
repositories.mavenInstaller {
pom.artifactId = 'feeds-uploads-reports-java-helper'
}
}
task execute(type: JavaExec) {
main = System.getProperty('mainClass')
classpath = sourceSets.main.runtimeClasspath
}
test {
jvmArgs('-Djavax.net.ssl.trustStore=resources/main/certs/InternalAndExternalTrustStore.jks',
'-Djavax.net.ssl.trustStorePassword=amazon')
}
test.dependsOn processTestResources
// TO get this to work,
// You must add the sellingpartner-api-aa-java.jar (with dependencies) to your
// /libs/ folder!!!!!
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.8'
annotationProcessor 'org.projectlombok:lombok:1.18.8'
implementation 'io.swagger:swagger-annotations:1.5.17'
implementation 'com.squareup.okhttp:okhttp:2.7.5'
implementation 'com.squareup.okhttp:logging-interceptor:2.7.5'
implementation 'com.google.code.gson:gson:2.8.1'
implementation 'io.gsonfire:gson-fire:1.8.0'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.10.2'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.2'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.10.2'
implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.12'
implementation group: 'org.apache.directory.studio', name: 'org.apache.commons.io', version: '2.4'
implementation group: 'com.google.guava', name: 'guava', version: '28.2-jre'
implementation group: 'joda-time', name: 'joda-time', version: '2.10.5'
// https://mvnrepository.com/artifact/org.threeten/threetenbp
implementation group: 'org.threeten', name: 'threetenbp', version: '1.3.5'
// https://mvnrepository.com/artifact/junit/junit
testCompile group: 'junit', name: 'junit', version: '4.12'
implementation 'com.amazonaws:aws-java-sdk-signer:1.11.610'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.squareup.okhttp:okhttp:2.7.5'
implementation 'org.junit.jupiter:junit-jupiter-engine:5.0.0'
implementation 'org.junit.jupiter:junit-jupiter-params:5.3.2'
implementation 'org.junit.jupiter:junit-jupiter-migrationsupport:5.5.1'
implementation 'org.mockito:mockito-core:3.0.0'
implementation 'org.apache.commons:commons-lang3:3.9'
implementation 'org.apache.httpcomponents:httpclient:4.5.9'
implementation fileTree(dir: 'libs', include: '*.jar')
}

View File

@ -0,0 +1,164 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>io.swagger</groupId>
<artifactId>feeds-uploads-reports-java-helper</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.17</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>logging-interceptor</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>io.gsonfire</groupId>
<artifactId>gson-fire</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.10.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.10.2</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.5</version>
</dependency>
<dependency>
<groupId>org.threeten</groupId>
<artifactId>threetenbp</artifactId>
<version>1.3.5</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-signer</artifactId>
<version>1.11.610</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.3.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-migrationsupport</artifactId>
<version>5.5.1</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.amazon.sellingpartnerapi</groupId>
<artifactId>sellingpartnerapi-aa-java</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.amazon.sellingpartnerapi</groupId>
<artifactId>sellingpartnerapi-feeds-java</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.amazon.sellingpartnerapi</groupId>
<artifactId>sellingpartnerapi-reports-java</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.amazon.sellingpartnerapi</groupId>
<artifactId>sellingpartnerapi-uploads-java</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven-plugin-version>1.0.0</maven-plugin-version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@ -0,0 +1 @@
rootProject.name = "feeds-uploads-reports-java-helper"

View File

@ -0,0 +1,64 @@
package com.amazon.spapi.sequencing.client;
import com.google.common.base.Preconditions;
import lombok.extern.apachecommons.CommonsLog;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* An input stream that validates the integrity of the document when you close the stream.
*/
@CommonsLog
public class DocumentValidatorInputStream extends FilterInputStream {
private final byte[] expectedSha256sum;
private final MessageDigest sha256digest;
/**
* Creates an InputStream that will calculate the sha256digest and validate it upon closing the stream.
* @param inputStream
* the inputStream to calculate the sha256sum of.
* @param expectedSha256sum
* the expected sha256sum of the document being streamed if one exists. Leave null if document does not
* have a sha256sum to validate against.
* @throws NoSuchAlgorithmException
* when unable to create a SHA-256 instance of MessageDigest
*/
public DocumentValidatorInputStream(InputStream inputStream, byte[] expectedSha256sum)
throws NoSuchAlgorithmException {
super(null);
Preconditions.checkArgument(inputStream != null, "inputStream must not be null");
Preconditions.checkArgument(expectedSha256sum != null, "expectedSha256sum must not be null");
this.expectedSha256sum = Arrays.copyOf(expectedSha256sum, expectedSha256sum.length);
sha256digest = MessageDigest.getInstance("SHA-256");
this.in = new DigestInputStream(inputStream, sha256digest);
}
/**
* Validates that the streamed document was not tampered with.
* @throws IOException
* if the streamed document was completely read and is not valid or an IOException occurs while closing
* the stream.
*/
@Override
public void close() throws IOException {
try {
if (!MessageDigest.isEqual(expectedSha256sum, sha256digest.digest()) && super.read() == -1) {
throw new ValidationFailureException("The streamed document was not valid.");
}
} finally {
try {
super.close();
} catch (IOException e) {
log.warn(String.format("Exception while closing the validation string: %s", e.getMessage()), e);
}
}
}
}

View File

@ -0,0 +1,23 @@
package com.amazon.spapi.sequencing.client;
import com.amazon.spapi.sequencing.crypto.EncryptionException;
import io.swagger.client.model.EncryptionDetails;
import io.swagger.client.model.UploadDestination;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public interface DownloadsSequencer {
InputStream downloadAndDecryptReportContent(UploadDestination destination, boolean isGzipped) throws IOException;
InputStream decryptReportContent(File reportFile, EncryptionDetails details, boolean isGzipped)
throws IOException, EncryptionException;
void downloadDecryptThenWriteFile(UploadDestination destination, boolean isGzipped, File fileToWriteTo)
throws IOException;
void decryptThenWriteFile(File reportFile, EncryptionDetails details, boolean isGzipped, File fileToWriteTo)
throws IOException, EncryptionException;
}

View File

@ -0,0 +1,19 @@
package com.amazon.spapi.sequencing.client;
import java.io.IOException;
/**
* Exception thrown when spooling size limits exceeded.
*/
public class SpoolingLimitExceededException extends IOException {
/**
* Constructor that takes in a message.
* @param message
* the message describing the exception
*/
public SpoolingLimitExceededException(String message) {
super(message);
}
}

View File

@ -0,0 +1,19 @@
package com.amazon.spapi.sequencing.client;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.experimental.FieldDefaults;
@Builder
@Getter
@ToString
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@EqualsAndHashCode
public class UploadDetails {
String sha256Sum;
String uploadDestinationId;
}

View File

@ -0,0 +1,46 @@
package com.amazon.spapi.sequencing.client;
import io.swagger.client.ApiException;
import io.swagger.client.model.UploadDestination;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
/**
* A thick client used to simplify the use of SP-API Uploads.
*/
public interface UploadsSequencer {
/**
* Retrieve a document and related metadata from SP-API Uploads.
* @param obfuscatedCustomerId the obfuscated customer ID of the document owner.
* @param documentId the document ID of the document being retrieved.
* @return A DownloadBundle to be used to retrieve the requested document.
* @throws RuntimeException if an exceptional case was encountered but the action should be retried.
* @throws RuntimeException if an exceptional case was encountered and the action should not be retried.
* @throws RuntimeException if an exceptional case was encountered and the cause was unexpected.
*/
// DownloadBundle get(String obfuscatedCustomerId, String documentId) throws RuntimeException;
UploadDestination createDestination(String feedType, String contentType, long documentLength)
throws ApiException;
/**
* Delivers a document and related metadata to SP-API Uploads.
* @param uploadDestination metadata for the document being uploaded.
* @param contentType the content type
* @param documentLength the length in bytes of the document.
* @param inputStream the InputStream from which the document should be read for upload.
* @return A String document ID.
* @throws RuntimeException if an exceptional case was encountered but the action should be retried.
* @throws RuntimeException if an exceptional case was encountered and the action should not be retried.
* @throws RuntimeException if an exceptional case was encountered and the cause was unexpected.
*/
UploadDetails uploadToDestination(UploadDestination uploadDestination, String contentType, long documentLength,
InputStream inputStream) throws RuntimeException;
UploadDetails createDestinationAndUpload(String feedType, String contentType, File fileName)
throws ApiException, FileNotFoundException;
UploadDetails createDestinationAndUpload(String feedType, String contentType, long documentLength,
InputStream stream) throws ApiException;
}

View File

@ -0,0 +1,38 @@
package com.amazon.spapi.sequencing.client;
/**
* Exception thrown if document integrity has been compromised during handling and is not considered safe to consume.
*/
public class ValidationFailureException extends RuntimeException {
/**
* Constructor taking both a message and cause.
* @param message
* the message
* @param cause
* the cause
*/
public ValidationFailureException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructor taking only a message.
* @param message
* the message
*/
public ValidationFailureException(String message) {
super(message);
}
/**
* Constructor taking only a cause.
* @param cause
* the cause
*/
public ValidationFailureException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,15 @@
package com.amazon.spapi.sequencing.client.impl;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
@AllArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@Getter
public enum AccessMechanism {
UPLOAD("upload"), DOWNLOAD("download");
private String description;
}

View File

@ -0,0 +1,307 @@
package com.amazon.spapi.sequencing.client.impl;
import com.amazon.spapi.sequencing.client.DownloadsSequencer;
import com.amazon.spapi.sequencing.crypto.EncryptionException;
import com.google.common.base.Strings;
import io.swagger.client.model.EncryptionDetails;
import io.swagger.client.model.UploadDestination;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.experimental.FieldDefaults;
import lombok.extern.apachecommons.CommonsLog;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import javax.crypto.Cipher;
import java.io.*;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import static com.amazon.spapi.sequencing.client.impl.SequencerHelper.S3_IAD_ENDPOINT;
import static com.amazon.spapi.sequencing.client.impl.SequencerHelper.S3_SEA_ENDPOINT;
@Builder
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@CommonsLog
public class DownloadsSequencerImpl implements DownloadsSequencer {
@Builder.Default
private final SequencerHelper helper = SequencerHelper.defaultSequencerHelper();
@Override
public InputStream downloadAndDecryptReportContent(UploadDestination destination, boolean isGzipped) {
boolean hasError = false;
InputStream result = null;
String truncatedUrl = null;
try {
String url = destination.getUrl();
if (Strings.isNullOrEmpty(url)) {
throw new RuntimeException("The returned download URL is null or empty.");
}
// Acquire the file
HttpClient httpClient = helper.getHttpClientFactory().newGetClient();
HttpGet httpGet = new HttpGet(url);
truncatedUrl = helper.removeQueryFromUrl(url);
log.info(String.format("Downloading document with destination ID %s using URL %s (signing info redacted)",
destination.getUploadDestinationId(), truncatedUrl
));
HttpResponse httpResponse = httpClient.execute(httpGet);
if (httpResponse != null && httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND) {
boolean isS3UsStandardRegion = false;
if (url.contains(S3_IAD_ENDPOINT)) {
isS3UsStandardRegion = true;
url = url.replaceFirst(Pattern.quote(S3_IAD_ENDPOINT), S3_SEA_ENDPOINT);
} else if (url.contains(S3_SEA_ENDPOINT)) {
isS3UsStandardRegion = true;
url = url.replaceFirst(Pattern.quote(S3_SEA_ENDPOINT), S3_IAD_ENDPOINT);
}
if (isS3UsStandardRegion) {
log.info("S3 Returned a 404 in the US Standard Region."
+ " This likely indicates issues with eventual consistency and should be rare."
+ " Attempting to find object in peer region."
+ " See: https://w.amazon.com/index.php/S3#What_About_Eventual_Consistency.3F");
// Consume the previous response so that the HTTP client connection can be reused
EntityUtils.consumeQuietly(httpResponse.getEntity());
httpGet = new HttpGet(url);
httpResponse = httpClient.execute(httpGet);
}
}
// Handle error responses
helper.handleErrorResponseCodes(httpResponse, AccessMechanism.DOWNLOAD);
HttpEntity entity = httpResponse.getEntity();
if (entity == null) {
throw new RuntimeException("The HTTP store returned success but no document.");
}
result = entity.getContent();
result = decryptReportContent(entity.getContent(), destination.getEncryptionDetails(), isGzipped);
return result;
} catch (ClientProtocolException e) {
// Indicates an error in the HTTP protocol
hasError = true;
throw new RuntimeException(
"Unable to download the file.", e);
} catch (EncryptionException e) {
// Unable to perform cipher operations
hasError = true;
throw new RuntimeException(
"Unable to perform cipher operations.", e);
} catch (IOException e) {
hasError = true;
// Unable to download the file or operate on the stream
throw new RuntimeException(
String.format("Unable to download the file using URL %s (signing info redacted) "
+ "or manipulate the InputStream.", truncatedUrl),
e
);
} finally {
// kill the stream if we are throwing an exception
if (hasError) {
IOUtils.closeQuietly(result);
}
}
}
@Override
public InputStream decryptReportContent(File reportFile, EncryptionDetails details, boolean isGzipped)
throws IOException, EncryptionException {
return decryptReportContent(new FileInputStream(reportFile), details, isGzipped);
}
private InputStream decryptReportContent(InputStream input, EncryptionDetails details, boolean isGzipped)
throws EncryptionException, IOException {
InputStream resultStream = input;
// If encrypted, decipher the stream
if (details != null && EncryptionDetails.StandardEnum.AES.equals(details.getStandard())) {
try {
resultStream = helper.buildCipherInputStream(details, resultStream, Cipher.DECRYPT_MODE);
} catch (IllegalArgumentException | IllegalStateException e) {
throw new EncryptionException(e);
}
}
// Determine if the stream should be unzipped as well
if (isGzipped) {
resultStream = new GZIPInputStream(resultStream);
}
return resultStream;
}
@Override
public void downloadDecryptThenWriteFile(UploadDestination destination, boolean isGzipped, File fileToWriteTo)
throws IOException {
try (
InputStream result = downloadAndDecryptReportContent(destination, isGzipped);
FileOutputStream output = new FileOutputStream(fileToWriteTo);
) {
IOUtils.copyLarge(result, output);
}
}
@Override
public void decryptThenWriteFile(File reportFile, EncryptionDetails details, boolean isGzipped,
File fileToWriteTo) throws IOException, EncryptionException {
try (
InputStream result = decryptReportContent(reportFile,details, isGzipped);
FileOutputStream output = new FileOutputStream(fileToWriteTo);
) {
IOUtils.copyLarge(result, output);
}
}
/*
*/
/** {@inheritDoc} *//*
@Override
public DownloadBundle get(String obfuscatedCustomerId, String documentId)
throws RuntimeException, RuntimeException, RuntimeException {
Preconditions.checkArgument(!Strings.isNullOrEmpty(obfuscatedCustomerId),
"obfuscatedMerchantCustomerId is null or empty.");
Preconditions.checkArgument(!Strings.isNullOrEmpty(documentId), "documentId is null or empty.");
Preconditions.checkState(httpClientFactory != null, "The httpClientFactory is null.");
// We need the expanded scope so that we can close the stream in the case of an exception.
InputStream resultStream = null;
DownloadBundle result = null;
String truncatedUrl = null;
try {
// Acquire the document metadata, document receipt URL and encryption details from Tortuga.
GetDownloadUrlInput getUrlInput = new GetDownloadUrlInput();
getUrlInput.setDocumentId(documentId);
getUrlInput.setObfuscatedCustomerId(obfuscatedCustomerId);
GetDownloadUrlOutput getUrlOutput =
GetDownloadUrlSyncJobBuilder.build(getUrlInput, metricsStrategy, client).run();
DocumentMetadata metadata = getUrlOutput.getDocumentMetadata();
if (metadata == null) {
throw new RuntimeException("The returned document metadata is null.");
}
String url = getUrlOutput.getUrl();
if (Strings.isNullOrEmpty(url)) {
throw new RuntimeException("The returned download URL is null or empty.");
}
Date creationTimestamp = getUrlOutput.getCreationTimestamp();
if (creationTimestamp == null) {
throw new RuntimeException("The returned document creationDate is null.");
}
getUrlOutput.setDocumentMetadata(null);
// Acquire the file
HttpClient httpClient = httpClientFactory.newGetClient();
HttpGet httpGet = new HttpGet(url);
truncatedUrl = removeQueryFromUrl(url);
log.info(String.format("Downloading document with ID %s using URL %s (signing info redacted)",
documentId, truncatedUrl));
long startTimeMillis = System.nanoTime() / 1000000L;
HttpResponse httpResponse = httpClient.execute(httpGet);
if (httpResponse != null && httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND) {
boolean isS3UsStandardRegion = false;
if (url.contains(S3_IAD_ENDPOINT)) {
isS3UsStandardRegion = true;
url = url.replaceFirst(Pattern.quote(S3_IAD_ENDPOINT), S3_SEA_ENDPOINT);
} else if (url.contains(S3_SEA_ENDPOINT)) {
isS3UsStandardRegion = true;
url = url.replaceFirst(Pattern.quote(S3_SEA_ENDPOINT), S3_IAD_ENDPOINT);
}
if (isS3UsStandardRegion) {
log.info("S3 Returned a 404 in the US Standard Region."
+ " This likely indicates issues with eventual consistency and should be rare."
+ " Attempting to find object in peer region."
+ " See: https://w.amazon.com/index.php/S3#What_About_Eventual_Consistency.3F");
// Consume the previous response so that the HTTP client connection can be reused
EntityUtils.consumeQuietly(httpResponse.getEntity());
httpGet = new HttpGet(url);
httpResponse = httpClient.execute(httpGet);
}
}
long endTimeMillis = System.nanoTime() / 1000000L;
serviceMetrics.addTime(S3_GET_TIME_METRIC, endTimeMillis - startTimeMillis, SI.MILLI(SI.SECOND));
// Handle error responses
handleErrorResponseCodes(httpResponse);
HttpEntity entity = httpResponse.getEntity();
if (entity == null) {
throw new RuntimeException("The HTTP store returned success but no document.");
}
resultStream = entity.getContent();
// If this document has a sha256sum in the metadata, wrap with DigestInputStream to calculate the sha256 sum
// for validation of document integrity
String sha256sumStr = getUrlOutput.getSha256sum();
if (sha256sumStr != null) {
resultStream = new DocumentValidatorInputStream(resultStream, BASE64_DECODER.decode(sha256sumStr));
}
// If encrypted, decipher the stream
if (getUrlOutput.isEncrypted()) {
try {
EncryptionInfo encryptionInfo = getUrlOutput.getEncryptionInfo();
resultStream = buildCipherInputStream(encryptionInfo, resultStream, Cipher.DECRYPT_MODE);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalStateException e) {
throw new RuntimeException(e);
}
}
// Determine if the stream should be unzipped as well
if (metadata.isDocumentGzipped()) {
resultStream = new GZIPInputStream(resultStream);
}
// build the result
result = new DownloadBundle(metadata, resultStream, creationTimestamp.getTime());
} catch (ClientProtocolException e) {
// Indicates an error in the HTTP protocol
throw new RuntimeException(
"Unable to download the file.", e);
} catch (EncryptionException e) {
// Unable to perform cipher operations
throw new RuntimeException(
"Unable to perform cipher operations.", e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Couldn't get a Sha256 Message Digest instance", e);
} catch (IOException e) {
// Unable to download the file or operate on the stream
throw new RuntimeException(
String.format("Unable to download the file using URL %s (signing info redacted) "
+ "or manipulate the InputStream.", truncatedUrl),
e);
} finally {
// kill the stream if we are throwing an exception
if (result == null) {
IOUtils.closeQuietly(resultStream);
}
}
return result;
}
*/
}

View File

@ -0,0 +1,19 @@
package com.amazon.spapi.sequencing.client.impl;
import org.apache.http.client.HttpClient;
/**
* Constructs purpose built HttpClient instances.
*/
public interface HttpClientFactory {
/**
* Constructs an HttpClient instance configured for GET requests.
* @return an HttpClient instance.
*/
HttpClient newGetClient();
/**
* Constructs an HttpClient instance configured for PUT requests.
* @return an HttpClient instance.
*/
HttpClient newPutClient();
}

View File

@ -0,0 +1,71 @@
package com.amazon.spapi.sequencing.client.impl;
import com.google.common.collect.ImmutableMap;
import lombok.Setter;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import java.util.Map;
/**
* HttpClientFactory implementation for SP-API Upload Destinations.
*/
@Setter
public class SPAPIUploadDestinationsHttpClientFactory implements HttpClientFactory {
private static final Map<String, Object> DEFAULT_CONNECTION_PARAMETERS;
static {
ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
builder.put(CoreConnectionPNames.CONNECTION_TIMEOUT, 10000);
builder.put(CoreConnectionPNames.SO_TIMEOUT, 10000);
builder.put(CoreConnectionPNames.SO_KEEPALIVE, Boolean.TRUE);
DEFAULT_CONNECTION_PARAMETERS = builder.build();
}
/**
*http://hc.apache.org/httpcomponents-core-4.2.x/httpcore/apidocs/org/apache/http/params/CoreConnectionPNames.html.
*/
private Map<String, Object> getClientConnectionParametersOverrides = DEFAULT_CONNECTION_PARAMETERS;
/**
*http://hc.apache.org/httpcomponents-core-4.2.x/httpcore/apidocs/org/apache/http/params/CoreConnectionPNames.html.
*/
private Map<String, Object> putClientConnectionParametersOverrides = DEFAULT_CONNECTION_PARAMETERS;
private String userAgent;
/** {@inheritDoc} */
@Override
public HttpClient newGetClient() {
return newHttpClient(getClientConnectionParametersOverrides);
}
/** {@inheritDoc} */
@Override
public HttpClient newPutClient() {
return newHttpClient(putClientConnectionParametersOverrides);
}
/**
* Helper function to create a new DefaultHttpClient with the given connectionParameter Overrides.
* @param connectionParameters used to override the default properties of the HttpClient.
* @return a new, configured HttpClient.
*/
private HttpClient newHttpClient(Map<String, Object> connectionParameters) {
// TODO: evaluate necessity
// Preconditions.checkState(!Strings.isNullOrEmpty(userAgent), "You must supply a User-Agent.");
DefaultHttpClient client = new DefaultHttpClient();
for (Map.Entry<String, Object> param : connectionParameters.entrySet()) {
client.getParams().setParameter(param.getKey(), param.getValue());
}
if (userAgent != null) {
client.getParams().setParameter(CoreProtocolPNames.USER_AGENT, userAgent);
}
return client;
}
}

View File

@ -0,0 +1,156 @@
package com.amazon.spapi.sequencing.client.impl;
import com.amazon.spapi.sequencing.crypto.CryptoProvider;
import com.amazon.spapi.sequencing.crypto.EncryptionException;
import com.amazon.spapi.sequencing.crypto.EncryptionMaterials;
import com.amazon.spapi.sequencing.crypto.SymmetricCryptoProvider;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import io.swagger.client.model.EncryptionDetails;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
import lombok.extern.apachecommons.CommonsLog;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpResponseException;
import org.apache.http.util.EntityUtils;
import javax.crypto.CipherInputStream;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Base64;
import java.util.Objects;
@Builder
@Getter
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@CommonsLog
public class SequencerHelper {
static final String SHA_256 = "SHA-256";
static final String MD5 = "MD5";
static final String S3_IAD_ENDPOINT = "s3-external-1.amazonaws.com";
static final String S3_SEA_ENDPOINT = "s3-external-2.amazonaws.com";
static final String S3_GET_TIME_METRIC = "S3GetTime";
static final String S3_PUT_TIME_METRIC = "S3PutTime";
static final String REQUIRED_KEY_ALGORITHM = "AES";
static final int AES_BLOCK_SIZE = 16;
static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder();
static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();
private CryptoProvider cryptoProvider;
private HttpClientFactory httpClientFactory;
public static SequencerHelper defaultSequencerHelper() {
return SequencerHelper.builder()
.cryptoProvider(new SymmetricCryptoProvider())
.httpClientFactory(new SPAPIUploadDestinationsHttpClientFactory())
.build();
}
/**
* Remove the query string from a signed S3 URL to hide access key, signature, expiration info when logging.
*
* @param signedS3Url Signed S3 URL to truncate
* @return truncated URL
*/
String removeQueryFromUrl(String signedS3Url) {
if (signedS3Url == null) {
return null;
}
try {
URL url = new URL(signedS3Url);
if (url.getQuery() == null) {
return signedS3Url;
} else {
return signedS3Url.replace(url.getQuery(), "");
}
} catch (MalformedURLException e) {
return null;
}
}
/**
* Wraps the provided InputStream in a CipherInputStream in DECRYPT mode.
*
* @param encryptionDetails The EncryptionInfo to use in constructing the Cipher.
* @param stream The InputStream to wrap.
* @param mode The cipher mode.
* @return The wrapped InputStream
* @throws EncryptionException if initializing the Cipher fails.
*/
InputStream buildCipherInputStream(EncryptionDetails encryptionDetails, InputStream stream, int mode)
throws EncryptionException {
Preconditions.checkArgument(!Objects.isNull(encryptionDetails), "Encryption details missing");
Preconditions.checkArgument(!Strings.isNullOrEmpty(encryptionDetails.getInitializationVector()),
"iv is null or empty."
);
Preconditions.checkArgument(!Strings.isNullOrEmpty(encryptionDetails.getKey()), "key is null or empty.");
Preconditions.checkState(cryptoProvider != null, "The CryptoProvider must be non-null.");
String key = encryptionDetails.getKey();
String iv = encryptionDetails.getInitializationVector();
EncryptionMaterials materials = new EncryptionMaterials(new SecretKeySpec(BASE64_DECODER.decode(key),
REQUIRED_KEY_ALGORITHM
), BASE64_DECODER.decode(iv));
return new CipherInputStream(stream, cryptoProvider.getInitializedCipher(mode, materials));
}
/**
* Examine the HttpResponse object and throw appropriate exceptions for non-success status
* codes.
*
* @param httpResponse the HttpResponse object to examine.
* @throws RuntimeException if the status code is not 200 level or 400 level (except 403 errors).
* @throws RuntimeException if the HttpResponse, or its nested StatusLine object are null. Or if the status
* code is a 400 level HTTP response code (except 403 errors, which are retriable).
*/
void handleErrorResponseCodes(HttpResponse httpResponse, AccessMechanism accessMechanism) throws RuntimeException {
// Handle error responses
if (httpResponse == null || httpResponse.getStatusLine() == null) {
throw new RuntimeException("Unable to " + accessMechanism.getDescription() + " the document.");
}
int status = httpResponse.getStatusLine().getStatusCode();
log.info(String.format("Document " + accessMechanism.getDescription() + " returned status code %d", status));
int statusRange = status / 100;
if (statusRange != 2) { // Not 2XX
HttpEntity entity = httpResponse.getEntity();
if (entity != null) {
try {
log.warn(EntityUtils.toString(entity, "UTF-8"));
} catch (IOException e) {
log.warn("Got IOException when trying to decode response body into a String", e);
}
}
String reason = httpResponse.getStatusLine().getReasonPhrase();
HttpResponseException error = new HttpResponseException(status, reason);
// A 403 response is typically due to the signed URL expiring before use.
// The root cause of the expiration is usually clock skew, so treat this as retriable.
if (status == HttpStatus.SC_FORBIDDEN) {
throw new RuntimeException(
String.format("A 403 response was returned. This typically indicates that the signed URL "
+ "expired before use (usually due to clock skew). %s %s", status, reason),
error
);
} else if (statusRange == 4) { // 4XX
throw new RuntimeException(
String.format("A 400 level response was returned: %s %s", status, reason), error);
} else { // 1XX, 3XX, 5XX
throw new RuntimeException(
String.format("A non 200/400 level response was returned: %s %s", status, reason), error);
}
}
}
}

View File

@ -0,0 +1,178 @@
package com.amazon.spapi.sequencing.client.impl;
import com.amazon.spapi.sequencing.client.UploadDetails;
import com.amazon.spapi.sequencing.client.UploadsSequencer;
import com.amazon.spapi.sequencing.crypto.EncryptionException;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import io.swagger.client.ApiException;
import io.swagger.client.api.UploadsApi;
import io.swagger.client.model.CreateUploadDestinationResponse;
import io.swagger.client.model.EncryptionDetails;
import io.swagger.client.model.UploadDestination;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.experimental.FieldDefaults;
import lombok.extern.apachecommons.CommonsLog;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.InputStreamEntity;
import javax.crypto.Cipher;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* This implementation performs the expected crypto and compression work on the provided streams.
*/
@Builder
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@CommonsLog
public class UploadsSequencerImpl implements UploadsSequencer {
@Builder.Default
private final SequencerHelper helper = SequencerHelper.defaultSequencerHelper();
private UploadsApi uploadsApi;
/**
* {@inheritDoc}
*/
@Override
public UploadDetails uploadToDestination(UploadDestination uploadDestination, String contentType,
long documentLength,
InputStream inputStream) throws RuntimeException {
Preconditions.checkArgument(uploadDestination != null, "documentMetadata is null.");
Preconditions.checkArgument(inputStream != null, "inputStream is null.");
Preconditions.checkArgument(documentLength >= 0, "documentLength must be at least zero.");
Preconditions.checkState(helper.getHttpClientFactory() != null, "The httpClientFactory is null.");
String truncatedUrl = null;
try {
final String uploadDestinationId = uploadDestination.getUploadDestinationId();
if (Strings.isNullOrEmpty(uploadDestinationId)) {
throw new RuntimeException("The returned uploadDestinationId is null or empty.");
}
EncryptionDetails encryptionDetails = uploadDestination.getEncryptionDetails();
if (encryptionDetails == null) {
throw new RuntimeException("The returned encryptionDetails is null.");
}
final String url = uploadDestination.getUrl();
if (Strings.isNullOrEmpty(url)) {
throw new RuntimeException("The returned upload URL is null or empty.");
}
// Wrap the stream with a CipherStream
inputStream = helper.buildCipherInputStream(encryptionDetails, inputStream, Cipher.ENCRYPT_MODE);
// Wrap the cipher stream with a sha256 digest stream.
DigestInputStream sha256sumStream = new DigestInputStream(inputStream, MessageDigest.getInstance(
SequencerHelper.SHA_256));
inputStream = sha256sumStream;
truncatedUrl = helper.removeQueryFromUrl(url);
// Put the file
HttpClient httpClient = helper.getHttpClientFactory().newPutClient();
HttpPut httpPut = new HttpPut(url);
// This content length calculation is specific to AES
long contentLength = (documentLength / SequencerHelper.AES_BLOCK_SIZE + 1) * SequencerHelper.AES_BLOCK_SIZE;
// This sets the Content-Length header since we specified the contentLength
HttpEntity document = new InputStreamEntity(inputStream, contentLength);
httpPut.setHeader("Content-Type", contentType);
httpPut.setEntity(document);
log.info(String.format("Uploading document with ID %s using URL %s (signing info redacted)",
uploadDestinationId, truncatedUrl
));
HttpResponse httpResponse = httpClient.execute(httpPut);
// Handle error responses
helper.handleErrorResponseCodes(httpResponse, AccessMechanism.UPLOAD);
log.info(String.format("Successfully uploaded document with ID %s", uploadDestinationId));
byte[] sha256sumDigest = sha256sumStream.getMessageDigest().digest();
String sha256sum = SequencerHelper.BASE64_ENCODER.encodeToString(sha256sumDigest);
return UploadDetails.builder()
.sha256Sum(sha256sum)
.uploadDestinationId(uploadDestinationId)
.build();
} catch (ClientProtocolException e) {
// Indicates an error in the HTTP protocol
throw new RuntimeException("Unable to upload the file.", e);
} catch (EncryptionException e) {
// Unable to perform cipher operations
throw new RuntimeException("Unable to perform cipher operations.", e);
} catch (IOException e) {
// Unable to upload the file or operate on the stream
throw new RuntimeException(
String.format("Unable to upload the file using URL %s (signing info redacted) "
+ "or manipulate the InputStream.", truncatedUrl),
e
);
} catch (NoSuchAlgorithmException e) {
// Unable to get a MAC on the encrypted doc
throw new IllegalStateException("Unable to create MessageDigest. Required algorithm not present.", e);
} finally {
IOUtils.closeQuietly(inputStream);
}
}
@Override
public UploadDetails createDestinationAndUpload(String feedType, String contentType, File fileName)
throws FileNotFoundException, ApiException {
long documentLength = fileName.length();
return createDestinationAndUpload(feedType, contentType, documentLength, new FileInputStream(fileName));
}
@Override
public UploadDetails createDestinationAndUpload(String feedType, String contentType, long documentLength,
InputStream stream) throws ApiException {
UploadDestination destination = createDestination(feedType, contentType, documentLength);
return uploadToDestination(destination, contentType, documentLength, stream);
}
public UploadDestination createDestination(String feedType, String contentType, long documentLength)
throws ApiException {
Preconditions.checkArgument(documentLength < (long) Integer.MAX_VALUE,
"Document length cannot exceed " + Integer.MAX_VALUE + " bytes."
);
CreateUploadDestinationResponse response = uploadsApi.createUploadDestinationForFeed(feedType, contentType,
(int) documentLength
);
return response.getPayload();
}
/*
TODO
public void submitFeed(UploadDetails uploadDetails) {
CompleteDocumentUploadInput completeUploadInput = new CompleteDocumentUploadInput();
completeUploadInput.setDocumentId(uploadDestinationId);
completeUploadInput.setObfuscatedCustomerId(uploadDestination.getObfuscatedCustomerId());
completeUploadInput.setCompleteUploadToken(getUploadUrlOutput.getCompleteUploadToken());
completeUploadInput.setSha256sum(sha256sum);
completeUploadInput.setContentMd5(contentMd5);
CompleteDocumentUploadSyncJobBuilder.build(completeUploadInput, metricsStrategy, client).runOnce();
}
*/
}

View File

@ -0,0 +1,93 @@
package com.amazon.spapi.sequencing.client.util;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.OutputStream;
/**
* {@code OutputStream} wrapper class that forwards data written to this stream to an associated stream and allows
* switching of the associated stream on the fly. Associated stream can be {@code null} in which case all operations
* are, essentially, NOOP.
*/
public class DemuxOutputStream extends OutputStream {
private OutputStream out;
/**
* Creates a new {@code DemuxOutputStream} wrapper around given output stream. Accepts {@code null}.
* @param stream
* the output stream (can be {@code null})
*/
public DemuxOutputStream(@Nullable OutputStream stream) {
out = stream;
}
/**
* Binds new output stream. Originally bound output stream will be flushed and returned. Accepts {@code null}.
* @param stream
* the new input stream to bind (can be {@code null})
* @return the previously bound input stream (can be {@code null})
* @throws IOException
* if there was an error flushing the currently bound stream
*/
@Nullable
public OutputStream bind(@Nullable OutputStream stream) throws IOException {
OutputStream tmp = out;
out = stream;
if (tmp != null) {
tmp.flush();
}
return tmp;
}
/**
* {@inheritDoc}
*/
@Override
public void close() throws IOException {
if (out != null) {
out.close();
}
}
/**
* {@inheritDoc}
*/
@Override
public void flush() throws IOException {
if (out != null) {
out.flush();
}
}
/**
* {@inheritDoc}
*/
@Override
public void write(byte[] data, int offset, int len) throws IOException {
if (out != null) {
out.write(data, offset, len);
}
}
/**
* {@inheritDoc}
*/
@Override
public void write(byte[] data) throws IOException {
if (out != null) {
out.write(data);
}
}
/**
* {@inheritDoc}
*/
@Override
public void write(int data) throws IOException {
if (out != null) {
out.write(data);
}
}
}

View File

@ -0,0 +1,133 @@
package com.amazon.spapi.sequencing.client.util;
import lombok.extern.apachecommons.CommonsLog;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.*;
/**
* An InputStream from a temporary file that deletes the file after it was completely read or closed.
*/
@CommonsLog
public class TempInputStream extends InputStream {
private final FileInputStream fileStream;
private final File tempFile;
/**
* Creates a new {@code TempInputStream} for a given file.
* @param file
* the file
* @throws FileNotFoundException
* if file doesn't exist
*/
public TempInputStream(File file) throws FileNotFoundException {
fileStream = new FileInputStream(file);
tempFile = file;
}
/**
* File cleanup.
*/
private void cleanup() {
IOUtils.closeQuietly(fileStream);
try {
FileUtils.forceDelete(tempFile);
} catch (FileNotFoundException e) {
// Ignore not found exceptions
return;
} catch (IOException e) {
log.warn("IOException when deleting TempInputStream file", e);
}
}
/**
* {@inheritDoc}
*/
@Override
public void close() throws IOException {
try {
fileStream.close();
} finally {
cleanup();
}
}
/**
* {@inheritDoc}
*/
@Override
public int read() throws IOException {
try {
int read = fileStream.read();
if (read == -1) {
cleanup();
}
return read;
} catch (IOException e) {
cleanup();
throw e;
}
}
/**
* {@inheritDoc}
*/
@Override
public int read(byte[] bytes, int off, int len) throws IOException {
try {
int read = fileStream.read(bytes, off, len);
if (read == -1) {
cleanup();
}
return read;
} catch (IOException e) {
cleanup();
throw e;
}
}
/**
* {@inheritDoc}
*/
@Override
public int read(byte[] bytes) throws IOException {
try {
int read = fileStream.read(bytes);
if (read == -1) {
cleanup();
}
return read;
} catch (IOException e) {
cleanup();
throw e;
}
}
/**
* {@inheritDoc}
*/
@Override
public int available() throws IOException {
return fileStream.available();
}
/**
* {@inheritDoc}
*/
@Override
public long skip(long n) throws IOException {
return fileStream.skip(n);
}
/**
* {@inheritDoc}
*/
@Override
protected void finalize() throws IOException {
close();
}
}

View File

@ -0,0 +1,195 @@
package com.amazon.spapi.sequencing.crypto;
import com.google.common.base.Preconditions;
import javax.annotation.Nonnull;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.InvalidKeyException;
import java.security.SignatureException;
/**
* An Asymmetric verison of {@link CryptoProvider} that uses RSA.
* Encryption is performed using RSA with PKCS1Padding.
* Signing is performed using SHA256withRSA.
*/
public class AsymmetricCryptoProvider implements CryptoProvider {
private static final String REQUIRED_KEY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final String ENCRYPTION_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public byte[] encrypt(@Nonnull byte[] plainText, @Nonnull EncryptionMaterials materials)
throws EncryptionException {
Preconditions.checkArgument(plainText != null, "plainText must be non-null.");
Cipher cipher = getInitializedCipher(Cipher.ENCRYPT_MODE, materials);
return applyCipher(cipher, plainText);
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public byte[] decrypt(@Nonnull byte[] cipherText, @Nonnull EncryptionMaterials materials)
throws EncryptionException {
Preconditions.checkArgument(cipherText != null, "cipherText must be non-null.");
Cipher cipher = getInitializedCipher(Cipher.DECRYPT_MODE, materials);
return applyCipher(cipher, cipherText);
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public byte[] sign(@Nonnull byte[] bytesToSign, @Nonnull EncryptionMaterials materials)
throws SignatureGenerationException {
Preconditions.checkArgument(bytesToSign != null, "bytesToSign must be non-null.");
PrivateKey key = validatePrivateKey(materials);
byte[] signatureBytes;
try {
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(key, new SecureRandom());
signature.update(bytesToSign);
signatureBytes = signature.sign();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(String.format("NoSuchAlgorithmException for %s", SIGNATURE_ALGORITHM), e);
} catch (InvalidKeyException e) {
throw new SignatureGenerationException(e);
} catch (SignatureException e) {
throw new SignatureGenerationException(e);
}
return signatureBytes;
}
/**
* {@inheritDoc}
*/
@Override
public void validateSignature(@Nonnull byte[] signatureBytes, @Nonnull byte[] bytesToSign,
@Nonnull EncryptionMaterials materials) throws SignatureGenerationException,
SignatureValidationException {
Preconditions.checkArgument(signatureBytes != null, "signatureBytes must be non-null.");
Preconditions.checkArgument(bytesToSign != null, "bytesToSign must be non-null.");
PublicKey key = validatePublicKey(materials);
try {
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(key);
signature.update(bytesToSign);
if (!signature.verify(signatureBytes)) {
throw new SignatureValidationException("Signature mismatch. Possible tampering attempt.");
}
} catch (InvalidKeyException e) {
throw new SignatureGenerationException(e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(String.format("NoSuchAlgorithmException for %s", SIGNATURE_ALGORITHM), e);
} catch (SignatureException e) {
throw new SignatureGenerationException(e);
}
}
/**
* Helper function to apply a cipher to some data and return the results. Performs exception mapping.
* @param cipher the Cipher to apply.
* @param text the text to apply the cipher to
* @return the text generated by applying the cipher to the input text
* @throws EncryptionException if there is a problem applying the cipher.
*/
@Nonnull
private byte[] applyCipher(@Nonnull Cipher cipher, @Nonnull byte[] text) throws EncryptionException {
try {
return cipher.doFinal(text);
} catch (IllegalBlockSizeException e) {
throw new EncryptionException(e);
} catch (BadPaddingException e) {
throw new EncryptionException(e);
}
}
/**
* {@inheritDoc}
*/
@Nonnull
public Cipher getInitializedCipher(int mode, @Nonnull EncryptionMaterials materials) throws EncryptionException {
Key key;
if (Cipher.DECRYPT_MODE == mode) {
key = validatePrivateKey(materials);
} else {
key = validatePublicKey(materials);
}
Cipher cipher;
try {
cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
cipher.init(mode, key, new SecureRandom());
} catch (InvalidKeyException e) {
throw new EncryptionException(e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(String.format("NoSuchAlgorithmException for %s", SIGNATURE_ALGORITHM), e);
} catch (NoSuchPaddingException e) {
throw new IllegalStateException(String.format("NoSuchPaddingException for %s", SIGNATURE_ALGORITHM), e);
}
return cipher;
}
/**
* Helper function to ensure EncryptionMaterials are valid and the Key matches a specific class.
* @param materials the Materials to validate
* @param clazz the expected class of the key.
* @param message The error message if the key is not of the expected class
* @return the key.
*/
@Nonnull
private Key validateKey(@Nonnull EncryptionMaterials materials, @Nonnull Class<? extends Key> clazz,
@Nonnull String message) {
Preconditions.checkArgument(materials != null, "Materials must be non-null.");
Preconditions.checkArgument(REQUIRED_KEY_ALGORITHM.equals(materials.getKey().getAlgorithm()),
"AsymmetricCryptoProvider requires an RSA key.");
Preconditions.checkArgument(clazz.isAssignableFrom(materials.getKey().getClass()), message);
return materials.getKey();
}
/**
* Helper function that validates that the EncryptionMaterials contain a valid PrivateKey.
* @param materials the materials to validate
* @return the PrivateKey
*/
@Nonnull
private PrivateKey validatePrivateKey(@Nonnull EncryptionMaterials materials) {
return (PrivateKey) validateKey(materials, PrivateKey.class, "Key must be of type PrivateKey.");
}
/**
* Helper function that validates that the EncryptionMaterials contain a valid PublicKey.
* @param materials the materials to validate
* @return the PublicKey
*/
@Nonnull
private PublicKey validatePublicKey(@Nonnull EncryptionMaterials materials) {
return (PublicKey) validateKey(materials, PublicKey.class, "Key must be of type PublicKey.");
}
}

View File

@ -0,0 +1,34 @@
package com.amazon.spapi.sequencing.crypto;
/**
* Base class for any Cryptographic exception that occurs.
*/
public class CryptoException extends Exception {
/**
* Constructor taking both a message and cause.
* @param message the message
* @param cause the cause
*/
public CryptoException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructor taking only a message.
* @param message the message
*/
public CryptoException(String message) {
super(message);
}
/**
* Constructor taking only a cause.
* @param cause the cause
*/
public CryptoException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,66 @@
package com.amazon.spapi.sequencing.crypto;
import javax.annotation.Nonnull;
import javax.crypto.Cipher;
/**
* This class of objects performs encryption, decryption, signing (or hmacing), and signature/hmac validation.
*/
public interface CryptoProvider {
/**
* Encrypt plain text into cipher text.
* @param plainText the plain text to encrypt.
* @param materials the EncryptionMaterials to use to perform the encryption
* @return the cipherText
* @throws EncryptionException if any problems occur during encryption.
*/
@Nonnull
byte[] encrypt(@Nonnull byte[] plainText, @Nonnull EncryptionMaterials materials) throws EncryptionException;
/**
* Decrypts the cipher text into plain text.
* @param cipherText the cipher text to decrypt
* @param materials the EncryptionMaterials to use to perform decryption
* @return the plain text
* @throws EncryptionException if any problems occur during decryption.
*/
@Nonnull
byte[] decrypt(@Nonnull byte[] cipherText, @Nonnull EncryptionMaterials materials) throws EncryptionException;
/**
* Create a signature or hmac based on the bytes provided.
* @param bytesToSign the bytes to use when calculating the signature or hmac
* @param materials the materials to use when generating the signature or hmac
* @return the signature/hmac bytes
* @throws SignatureGenerationException if any problems occur during signing/hmacing
*/
@Nonnull
byte[] sign(@Nonnull byte[] bytesToSign, @Nonnull EncryptionMaterials materials)
throws SignatureGenerationException;
/**
* Validate that a signature/hmac matches the data that was signed.
* @param signature the signature/hmac to validate
* @param bytesToSign the data that was originally signed/hmac'd
* @param materials the materials to use when validating the signature or hmac
* @throws SignatureGenerationException if we are unable to generate the signature/hmac to validate against.
* @throws SignatureValidationException if the validation fails (i.e, the signature or hmac do not match).
* This indicates data corruption or a possible tampering attack.
*/
@Nonnull
void validateSignature(@Nonnull byte[] signature, @Nonnull byte[] bytesToSign,
@Nonnull EncryptionMaterials materials) throws SignatureGenerationException,
SignatureValidationException;
/**
* Correctly Initialize a Cipher given the mode. The mode is specified by {@link Cipher}'s Mode constants.
* @param mode The mode of the cipher (i.e., encrypt or decrypt). See {@link Cipher}'s Mode constants.
* @param materials The EncryptionMaterials to initialize the cipher with
* @return an initialized Cipher object
* @throws EncryptionException if the Cipher could not be initialized.
*/
@Nonnull
Cipher getInitializedCipher(int mode, @Nonnull EncryptionMaterials materials) throws EncryptionException;
}

View File

@ -0,0 +1,77 @@
package com.amazon.spapi.sequencing.crypto;
import com.google.common.base.Preconditions;
import javax.annotation.Nonnull;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* Default implementation of the {@link KeyConverter} interface.
*/
public class DefaultKeyConverter implements KeyConverter {
private static final String SECRET_KEY_ALGORITHM = "AES";
private static final String KEYPAIR_ALGORITHM = "RSA";
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public SecretKey convertToSymmetricKey(@Nonnull byte[] material) {
Preconditions.checkArgument(material != null, "material must be non-null.");
return new SecretKeySpec(material, SECRET_KEY_ALGORITHM);
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public PrivateKey convertToPrivateKey(@Nonnull byte[] material) throws com.amazon.spapi.sequencing.crypto.InvalidKeyException {
Preconditions.checkArgument(material != null, "material must be non-null.");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(material);
try {
KeyFactory factory = KeyFactory.getInstance(KEYPAIR_ALGORITHM);
return factory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("RSA Algorithm is invalid. This indicates a badly configured Provider.");
} catch (InvalidKeySpecException e) {
throw new com.amazon.spapi.sequencing.crypto.InvalidKeyException("Invalid key.", e);
}
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public PublicKey convertToPublicKey(@Nonnull byte[] material) throws com.amazon.spapi.sequencing.crypto.InvalidKeyException {
Preconditions.checkArgument(material != null, "material must be non-null.");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(material);
try {
KeyFactory factory = KeyFactory.getInstance(KEYPAIR_ALGORITHM);
return factory.generatePublic(keySpec);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("RSA Algorithm is invalid. This indicates a badly configured Provider.");
} catch (InvalidKeySpecException e) {
throw new com.amazon.spapi.sequencing.crypto.InvalidKeyException("Invalid key.", e);
}
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public KeyPair convertToKeyPair(@Nonnull byte[] publicMaterial, @Nonnull byte[] privateMaterial)
throws InvalidKeyException {
return new KeyPair(convertToPublicKey(publicMaterial), convertToPrivateKey(privateMaterial));
}
}

View File

@ -0,0 +1,32 @@
package com.amazon.spapi.sequencing.crypto;
/**
* An exception that indicates an error occured during encryption or decryption of some data.
*/
public class EncryptionException extends CryptoException {
/**
* Constructor taking both a message and cause.
* @param message the message
* @param cause the cause
*/
public EncryptionException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructor taking only a message.
* @param message the message
*/
public EncryptionException(String message) {
super(message);
}
/**
* Constructor taking only a cause.
* @param cause the cause
*/
public EncryptionException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,60 @@
package com.amazon.spapi.sequencing.crypto;
import com.google.common.base.Preconditions;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.security.Key;
import java.util.Arrays;
/**
* Key and initialization vector for use when encrypting things.
*/
@EqualsAndHashCode
public class EncryptionMaterials {
@Nonnull
@Getter
private final Key key;
@Nullable
private final byte[] initializationVector;
/**
* All Args Constructor.
* @param key non null Key for this EncryptionMaterials
* @param initializationVector Nullable initializationVector (used for CBC mode encryption).
*/
public EncryptionMaterials(@Nonnull Key key, @Nullable byte[] initializationVector) {
Preconditions.checkArgument(key != null, "Key must be non-null.");
this.key = key;
this.initializationVector = copyInitializationVector(initializationVector);
}
/**
* Get a copy of the initialization vector bytes.
* @return A copy of the initialization vector bytes.
*/
@Nullable
public byte[] getInitializationVector() {
return copyInitializationVector(initializationVector);
}
/**
* A null-safe helper that copies the initialization vector bytes into a new array if the IV is non-null.
* @param iv the byte array to copy
* @return a copy of iv if iv is non null, otherwise null.
*/
@Nullable
private byte[] copyInitializationVector(byte[] iv) {
byte[] newInitializationVector = null;
if (iv != null) {
newInitializationVector = Arrays.copyOf(iv, iv.length);
}
return newInitializationVector;
}
}

View File

@ -0,0 +1,34 @@
package com.amazon.spapi.sequencing.crypto;
/**
* InvalidKeyException indicates that a key was invalid.
*
*/
public class InvalidKeyException extends CryptoException {
/**
* Constructor taking both a message and cause.
* @param message the message
* @param cause the cause
*/
public InvalidKeyException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructor taking only a message.
* @param message the message
*/
public InvalidKeyException(String message) {
super(message);
}
/**
* Constructor taking only a cause.
* @param cause the cause
*/
public InvalidKeyException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,50 @@
package com.amazon.spapi.sequencing.crypto;
import javax.annotation.Nonnull;
import javax.crypto.SecretKey;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* A class of objects that converts the raw bytes of an encoded key into a Key.
*/
public interface KeyConverter {
/**
* Convert a byte[] into a SecretKey.
* @param material the byte[] to covert
* @return the SecretKey
*/
@Nonnull
SecretKey convertToSymmetricKey(@Nonnull byte[] material);
/**
* Convert a PKCS8 encoded byte[] into a PrivateKey.
* @param material the byte[] to covert
* @return the PrivateKey
* @throws InvalidKeyException if the byte[] is not a properly encoded key
*/
@Nonnull
PrivateKey convertToPrivateKey(@Nonnull byte[] material) throws InvalidKeyException;
/**
* Convert a X509 encoded byte[] into a PublicKey.
* @param material the byte[] to covert
* @return the PublicKey
* @throws InvalidKeyException if the byte[] is not a properly encoded key
*/
@Nonnull
PublicKey convertToPublicKey(@Nonnull byte[] material) throws InvalidKeyException;
/**
* Convert a X509 encoded byte[] and a PKCS8 encoded byte[] into a KeyPair.
* @param publicMaterial the X509 encoded byte[] to covert into the public key
* @param privateMaterial the PKCS8 encoded byte[] to covert into the private key
* @return the SecretKey
* @throws InvalidKeyException if either the public or private material is invalid
*/
@Nonnull
KeyPair convertToKeyPair(@Nonnull byte[] publicMaterial, @Nonnull byte[] privateMaterial)
throws InvalidKeyException;
}

View File

@ -0,0 +1,33 @@
package com.amazon.spapi.sequencing.crypto;
/**
* Base class for exceptions related to Signature/HMAC problems.
*/
public class SignatureException extends CryptoException {
/**
* Constructor taking both a message and cause.
* @param message the message
* @param cause the cause
*/
public SignatureException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructor taking only a message.
* @param message the message
*/
public SignatureException(String message) {
super(message);
}
/**
* Constructor taking only a cause.
* @param cause the cause
*/
public SignatureException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,33 @@
package com.amazon.spapi.sequencing.crypto;
/**
* Indicates that an error occured when generating a Signature or HMAC.
*/
public class SignatureGenerationException extends SignatureException {
/**
* Constructor taking both a message and cause.
* @param message the message
* @param cause the cause
*/
public SignatureGenerationException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructor taking only a message.
* @param message the message
*/
public SignatureGenerationException(String message) {
super(message);
}
/**
* Constructor taking only a cause.
* @param cause the cause
*/
public SignatureGenerationException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,34 @@
package com.amazon.spapi.sequencing.crypto;
/**
* Indicates that the signature/hmac did not verify and that there was a mismatch.
* This indicates either data corruption or a possible tamper attack.
*/
public class SignatureValidationException extends SignatureException {
/**
* Constructor taking both a message and cause.
* @param message the message
* @param cause the cause
*/
public SignatureValidationException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructor taking only a message.
* @param message the message
*/
public SignatureValidationException(String message) {
super(message);
}
/**
* Constructor taking only a cause.
* @param cause the cause
*/
public SignatureValidationException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,158 @@
package com.amazon.spapi.sequencing.crypto;
import com.google.common.base.Preconditions;
import lombok.extern.apachecommons.CommonsLog;
import javax.annotation.Nonnull;
import javax.crypto.Mac;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.InvalidKeyException;
import java.util.Arrays;
/**
* A Symmetric verison of {@link CryptoProvider} that uses AES and HmacSha256.
* Encryption is performed using AES/CBC/PKCS5Padding
* HMAC is performed using HmacSha256.
*/
@CommonsLog
public class SymmetricCryptoProvider implements CryptoProvider {
private static final String REQUIRED_KEY_ALGORITHM = "AES";
private static final String ENCRYPTION_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String MAC_ALGORITHM = "HmacSha256";
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public byte[] encrypt(@Nonnull byte[] plainText, @Nonnull EncryptionMaterials materials)
throws EncryptionException {
Preconditions.checkArgument(plainText != null, "plainText must be non-null.");
Cipher cipher = getInitializedCipher(Cipher.ENCRYPT_MODE, materials);
return applyCipher(cipher, plainText);
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public byte[] decrypt(@Nonnull byte[] cipherText, @Nonnull EncryptionMaterials materials)
throws EncryptionException {
Preconditions.checkArgument(cipherText != null, "cipherText must be non-null.");
Cipher cipher = getInitializedCipher(Cipher.DECRYPT_MODE, materials);
return applyCipher(cipher, cipherText);
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public byte[] sign(@Nonnull byte[] bytesToSign, @Nonnull EncryptionMaterials materials)
throws SignatureGenerationException {
Preconditions.checkArgument(bytesToSign != null, "bytesToSign must be non-null..");
validateMaterials(materials, false);
Mac mac;
try {
mac = Mac.getInstance(MAC_ALGORITHM);
mac.init(materials.getKey());
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(String.format("NoSuchAlgorithmException for %s", MAC_ALGORITHM), e);
} catch (InvalidKeyException e) {
throw new SignatureGenerationException(e);
}
return mac.doFinal(bytesToSign);
}
/**
* {@inheritDoc}
*/
@Override
public void validateSignature(@Nonnull byte[] signature, @Nonnull byte[] bytesToSign,
@Nonnull EncryptionMaterials materials) throws SignatureValidationException, SignatureGenerationException {
Preconditions.checkArgument(signature != null, "signature must be non-null.");
Preconditions.checkArgument(bytesToSign != null, "bytesToSign must be non-null.");
validateMaterials(materials, false);
byte[] expectedSignature = sign(bytesToSign, materials);
if (!MessageDigest.isEqual(signature, expectedSignature)) {
if (log.isTraceEnabled()) {
log.trace(String.format("Signature mismatch detected. Expected=%s, Actual=%s",
Arrays.toString(expectedSignature), Arrays.toString(signature)));
}
throw new SignatureValidationException("Signature mismatch. Possible tampering attempt.");
}
}
/**
* Helper function to apply a cipher to some data and return the results. Performs exception mapping.
* @param cipher the Cipher to apply.
* @param text the text to apply the cipher to
* @return the text generated by applying the cipher to the input text
* @throws EncryptionException if there is a problem applying the cipher.
*/
@Nonnull
private byte[] applyCipher(@Nonnull Cipher cipher, @Nonnull byte[] text) throws EncryptionException {
try {
return cipher.doFinal(text);
} catch (IllegalBlockSizeException e) {
throw new EncryptionException(e);
} catch (BadPaddingException e) {
throw new EncryptionException(e);
}
}
/**
* {@inheritDoc}
*/
@Nonnull
public Cipher getInitializedCipher(int mode, @Nonnull EncryptionMaterials materials) throws EncryptionException {
validateMaterials(materials, true);
Cipher cipher;
try {
cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
cipher.init(mode, materials.getKey(),
new IvParameterSpec(materials.getInitializationVector()), new SecureRandom());
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(String.format("NoSuchAlgorithmException for %s", ENCRYPTION_ALGORITHM), e);
} catch (NoSuchPaddingException e) {
throw new IllegalStateException(String.format("NoSuchPaddingException for %s", ENCRYPTION_ALGORITHM), e);
} catch (InvalidKeyException e) {
throw new EncryptionException(e);
} catch (InvalidAlgorithmParameterException e) {
throw new EncryptionException(e);
}
return cipher;
}
/**
* Helper function to ensure EncryptionMaterials are valid.
* @param materials the Materials to validate
* @param ivRequired whether or not this function should treat the InitializationVector as required.
* The IV is required for encryption/decryption but not for HMAC operations.
*/
private void validateMaterials(@Nonnull EncryptionMaterials materials, boolean ivRequired) {
Preconditions.checkArgument(materials != null, "Materials must be non-null.");
Preconditions.checkArgument(REQUIRED_KEY_ALGORITHM.equals(materials.getKey().getAlgorithm()),
"SymmetricCryptoProvider requires an AES Key.");
Preconditions.checkArgument(!ivRequired || materials.getInitializationVector() != null, "IV must be non-null.");
}
}

View File

@ -0,0 +1,107 @@
package com.amazon.spapi;
import com.amazon.SellingPartnerAPIAA.AWSAuthenticationCredentials;
import com.amazon.SellingPartnerAPIAA.LWAAuthorizationCredentials;
import com.amazon.spapi.sequencing.client.UploadDetails;
import com.amazon.spapi.sequencing.client.UploadsSequencer;
import com.amazon.spapi.sequencing.client.impl.UploadsSequencerImpl;
import io.swagger.client.ApiException;
import io.swagger.client.api.UploadsApi;
import org.junit.Ignore;
import org.junit.Test;
import java.io.File;
import java.io.FileNotFoundException;
import java.net.URL;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* API tests for UploadsApi
*/
public class CreateDestinationAndUploadTest {
private final String feedType = "_POST_PRODUCT_DATA_";
private final UploadsSequencer sequencer;
{
sequencer = testSequencer();
// Fill out the API sequencer with your credentials here.
}
static {
System.setProperty("javax.net.ssl.trustStore",
getResourceFile("certs/InternalAndExternalTrustStore.jks").getPath()
);
System.setProperty("javax.net.ssl.trustStorePassword", "amazon");
}
private UploadsSequencer createSequencer(String accessKeyId, String secretKey, String clientId, String clientSecret,
String refreshToken) {
AWSAuthenticationCredentials
awsAuthenticationCredentials = AWSAuthenticationCredentials.builder()
.accessKeyId(accessKeyId)
.secretKey(secretKey)
.region("us-west-2")
.build();
LWAAuthorizationCredentials lwaAuthorizationCredentials =
LWAAuthorizationCredentials.builder()
.clientId(clientId)
.clientSecret(clientSecret)
.refreshToken(
refreshToken)
.endpoint("https://api.integ.amazon.com/auth/O2/token")
.build();
UploadsApi api = new UploadsApi.Builder().awsAuthenticationCredentials(awsAuthenticationCredentials)
.lwaAuthorizationCredentials(lwaAuthorizationCredentials)
.endpoint("https://marketplaceapi-beta.amazonservices.com")
.build();
return UploadsSequencerImpl.builder()
.uploadsApi(api)
.build();
}
private static File getResourceFile(String fileName) {
final URL fileUrl = UploadsApi.class.getClassLoader().getResource(fileName);
assertNotNull(fileName + " cannot be reached", fileUrl);
final File file = new File(fileUrl.getFile());
assertTrue(file.exists());
return file;
}
@Test
@Ignore
public void createDestinationThenUpload() throws FileNotFoundException, ApiException {
//file must be in "test/resources"
final File flatFileInventoryLoader = getResourceFile("Feed_101__POST_PRODUCT_DATA_.xml");
final UploadDetails details = sequencer.createDestinationAndUpload(feedType,
"text/xml",
flatFileInventoryLoader
);
System.out.println("SHA256Sum = " + details.getSha256Sum() + " // UploadDestinationId = " + details.getUploadDestinationId());
assertNotNull(details);
assertNotNull(details.getUploadDestinationId());
}
@Test
@Ignore
public void makeItPass() {
assertTrue(true);
}
private UploadsSequencer testSequencer() {
return createSequencer("",
"",
"",
"",
""
);
}
}

View File

@ -0,0 +1,70 @@
package com.amazon.spapi;
import com.amazon.spapi.sequencing.client.DownloadsSequencer;
import com.amazon.spapi.sequencing.client.impl.DownloadsSequencerImpl;
import io.swagger.client.model.EncryptionDetails;
import io.swagger.client.model.UploadDestination;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class ReportDownloadTest {
private final DownloadsSequencer sequencer;
private UploadDestination destinationToDownload;
{
sequencer = DownloadsSequencerImpl.builder().build();
// Fill out the API sequencer with your credentials here.
}
static {
System.setProperty("javax.net.ssl.trustStore",
getResourceFile("certs/InternalAndExternalTrustStore.jks").getPath()
);
System.setProperty("javax.net.ssl.trustStorePassword", "amazon");
}
@Before
public void before() {
// TODO: fill this out based on payload
EncryptionDetails encryptionDetails = new EncryptionDetails().standard(EncryptionDetails.StandardEnum.AES)
.initializationVector("")
.key("");
destinationToDownload = new UploadDestination().encryptionDetails(encryptionDetails)
.url("");
}
@Test
// TODO: remove @Ignore when ready to test
@Ignore
public void downloadAndOutputDecryptedReport() throws IOException {
File fileToOutputTo = new File("DecryptedReport.xml");
sequencer.downloadDecryptThenWriteFile(destinationToDownload, false, fileToOutputTo);
}
@Test
public void makeItPass() {
assertTrue(true);
}
private static File getResourceFile(String fileName) {
final URL fileUrl = DownloadsSequencerImpl.class.getClassLoader().getResource(fileName);
assertNotNull(fileName + " cannot be reached", fileUrl);
final File file = new File(fileUrl.getFile());
assertTrue(file.exists());
return file;
}
}