Skip to content

Instantly share code, notes, and snippets.

@brianmfear
Last active February 3, 2022 09:47
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brianmfear/db444a87f35a5a6348784a683b4cbd18 to your computer and use it in GitHub Desktop.
Save brianmfear/db444a87f35a5a6348784a683b4cbd18 to your computer and use it in GitHub Desktop.
AWS SQS Methods, in Apex Code.

Example Usage

try {
    AWSSQS.sendMessage(someQueue, someStringMessage)
} catch(AWS.ServiceException ex) {
  // Perform appropriate handling here
}
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<customSettingsType>Hierarchy</customSettingsType>
<enableFeeds>false</enableFeeds>
<fields>
<fullName>AccessKey__c</fullName>
<externalId>false</externalId>
<label>Access Key</label>
<length>20</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>DeleteQueue__c</fullName>
<externalId>false</externalId>
<label>Delete Queue</label>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Url</type>
</fields>
<fields>
<fullName>DispatchQueue__c</fullName>
<externalId>false</externalId>
<label>Dispatch Queue</label>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Url</type>
</fields>
<fields>
<fullName>Endpoint__c</fullName>
<externalId>false</externalId>
<label>Endpoint</label>
<length>32</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>Region__c</fullName>
<externalId>false</externalId>
<label>Region</label>
<length>16</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>ScaleWidths__c</fullName>
<externalId>false</externalId>
<label>ScaleWidths</label>
<length>255</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>SecretKey__c</fullName>
<externalId>false</externalId>
<label>Secret Key</label>
<length>40</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<label>AmazonSQS</label>
<visibility>Protected</visibility>
</CustomObject>
public abstract class AWS {
// Post initialization logic (after constructor, before call)
protected abstract void init();
// XML Node utility methods that will help read elements
public static Boolean getChildNodeBoolean(Dom.XmlNode node, String ns, String name) {
try {
return Boolean.valueOf(node.getChildElement(name, ns).getText());
} catch(Exception e) {
return null;
}
}
public static DateTime getChildNodeDateTime(Dom.XmlNode node, String ns, String name) {
try {
return (DateTime)JSON.deserialize(node.getChildElement(name, ns).getText(), DateTime.class);
} catch(Exception e) {
return null;
}
}
public static Integer getChildNodeInteger(Dom.XmlNode node, String ns, String name) {
try {
return Integer.valueOf(node.getChildElement(name, ns).getText());
} catch(Exception e) {
return null;
}
}
public static String getChildNodeText(Dom.XmlNode node, String ns, String name) {
try {
return node.getChildElement(name, ns).getText();
} catch(Exception e) {
return null;
}
}
// Turns an Amazon exception into something we can present to the user/catch
public class ServiceException extends Exception {
public String Code, Message, Resource, RequestId;
public ServiceException(Dom.XmlNode node) {
String ns = node.getNamespace();
Code = getChildNodeText(node, ns, 'Code');
Message = getChildNodeText(node, ns, 'Message');
Resource = getChildNodeText(node, ns, 'Resource');
RequestId = getChildNodeText(node, ns, 'RequestId');
setMessage(Message);
}
public String toString() {
return JSON.serialize(this);
}
}
// Things we need to know about the service. Set these values in init()
protected String host, region, service, resource, accessKey, payloadSha256, endpointUrl;
public Url endpoint { get { return endpointUrl==null? null: new Url(endpointUrl); } set { endpointUrl = value==null? null: value.toExternalForm(); } }
protected HttpMethod method;
protected Blob payload;
protected DateTime requestTime;
// Not used externally, so we hide these values
Blob signingKey;
Map<String, String> queryParams, headerParams;
// Make sure we can't misspell methods
public enum HttpMethod { XGET, XPUT, XHEAD, XOPTIONS, XDELETE, XPOST }
// Add a header
protected void setHeader(String key, String value) {
headerParams.put(key.toLowerCase(), value);
}
// Add a query param
protected void setQueryParam(String key, String value) {
queryParams.put(key, uriEncode(value));
}
// Call this constructor with super() in subclasses
protected AWS() {
requestTime = DateTime.now();
queryParams = new Map<String, String>();
headerParams = new Map<String, String>();
}
// Create a canonical query string (used during signing)
String createCanonicalQueryString() {
String[] results = new String[0], keys = new List<String>(queryParams.keySet());
keys.sort();
for(String key: keys) {
results.add(key+'='+queryParams.get(key));
}
return String.join(results, '&');
}
// Create the canonical headers (used for signing)
String createCanonicalHeaders(String[] keys) {
keys.addAll(headerParams.keySet());
keys.sort();
String[] results = new String[0];
for(String key: keys) {
results.add(key+':'+headerParams.get(key));
}
return String.join(results, '\n')+'\n';
}
// Create the entire canonical request
String createCanonicalRequest(String[] headerKeys) {
return String.join(
new String[] {
method.name().removeStart('X'), // METHOD
new Url(endPoint, resource).getPath(), // RESOURCE
createCanonicalQueryString(), // CANONICAL QUERY STRING
createCanonicalHeaders(headerKeys), // CANONICAL HEADERS
String.join(headerKeys, ';'), // SIGNED HEADERS
payloadSha256 // SHA256 PAYLOAD
},
'\n'
);
}
// We have to replace ~ and " " correctly, or we'll break AWS on those two characters
protected string uriEncode(String value) {
return value==null? null: EncodingUtil.urlEncode(value, 'utf-8').replaceAll('%7E','~').replaceAll('\\+','%20');
}
// Create the entire string to sign
String createStringToSign(String[] signedHeaders) {
String result = createCanonicalRequest(signedHeaders);
return String.join(
new String[] {
'AWS4-HMAC-SHA256',
headerParams.get('date'),
String.join(new String[] { requestTime.formatGMT('yyyyMMdd'), region, service, 'aws4_request' },'/'),
EncodingUtil.convertToHex(Crypto.generateDigest('sha256', Blob.valueof(result)))
},
'\n'
);
}
// Create our signing key
protected void createSigningKey(String secretKey) {
signingKey = Crypto.generateMac('hmacSHA256', Blob.valueOf('aws4_request'),
Crypto.generateMac('hmacSHA256', Blob.valueOf(service),
Crypto.generateMac('hmacSHA256', Blob.valueOf(region),
Crypto.generateMac('hmacSHA256', Blob.valueOf(requestTime.formatGMT('yyyyMMdd')), Blob.valueOf('AWS4'+secretKey))
)
)
);
}
// Create all of the bits and pieces using all utility functions above
HttpRequest createRequest() {
init();
if(payload == null) {
payload = Blob.valueOf('');
}
payloadSha256 = EncodingUtil.convertToHex(Crypto.generateDigest('sha-256', payload));
setHeader('x-amz-content-sha256', payloadSha256);
if(host == null) {
host = endpoint.getHost();
}
setHeader('host', host);
HttpRequest request = new HttpRequest();
request.setMethod(method.name().removeStart('X'));
if(payload.size() > 0) {
setHeader('Content-Length', String.valueOf(payload.size()));
request.setBodyAsBlob(payload);
}
String
finalEndpoint = new Url(endpoint, resource).toExternalForm(),
queryString = createCanonicalQueryString();
if(queryString != '') {
finalEndpoint += '?'+queryString;
}
request.setEndpoint(finalEndpoint);
for(String key: headerParams.keySet()) {
request.setHeader(key, headerParams.get(key));
}
String[] headerKeys = new String[0];
String stringToSign = createStringToSign(headerKeys);
request.setHeader(
'Authorization',
String.format(
'AWS4-HMAC-SHA256 Credential={0},SignedHeaders={1},Signature={2}',
new String[] {
String.join(new String[] { accessKey, requestTime.formatGMT('yyyyMMdd'), region, service, 'aws4_request' },'/'),
String.join(headerKeys,';'), EncodingUtil.convertToHex(Crypto.generateMac('hmacSHA256', Blob.valueOf(stringToSign), signingKey))}
));
return request;
}
// Actually perform the request, and throw exception if response code is not valid
protected HttpResponse sendRequest(Set<Integer> validCodes) {
HttpResponse response = new Http().send(createRequest());
if(!validCodes.contains(response.getStatusCode())) {
if(response.getBody() != null) {
throw new ServiceException(response.getBodyDocument().getRootElement());
}
}
return response;
}
// Same as above, but assume that only 200 is valid
// This method exists because most of the time, 200 is what we expect
protected HttpResponse sendRequest() {
return sendRequest(new Set<Integer> { 200 });
}
}
public class AWSSQS {
abstract class Core extends AWS {
Core() {
super();
method = HttpMethod.XGET;
payload = Blob.valueOf('');
}
public virtual override void init() {
AmazonSQS__c configSettings = AmazonSQS__c.getOrgDefaults();
endpoint = new Url('https://'+configSettings.Endpoint__c);
accessKey = configSettings.AccessKey__c;
region = configSettings.Region__c;
service = 'sqs';
host = String.join(new String[] { service, region, 'amazonaws.com' },'.');
setHeader('Date', requestTime.formatGmt('yyyyMMdd\'T\'HHmmss\'Z\''));
setQueryParam('Version','2012-11-05');
setQueryParam('AWSAccessKeyID', accessKey);
setQueryParam('Timestamp', requestTime.formatGMT('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''));
setQueryParam('SignatureVersion', '4');
setQueryParam('SignatureMethod','HmacSHA256');
setHeader('Accept','application/json');
// Prevent leaking the secret key by only exposing the signing key
createSigningKey(configSettings.SecretKey__c);
}
}
public class ListQueuesRequest extends Core {
ListQueuesRequest() {
super();
resource = '/';
setQueryParam('Action', 'ListQueues');
}
public ListQueuesResult execute() {
String jsonBody = sendRequest().getBody();
return ((ListQueueResponse)JSON.deserialize(jsonBody, ListQueueResponse.class)).ListQueuesResponse.ListQueuesResult;
}
}
public class SendMessageRequest extends Core {
public void execute(String queueName, String data) {
resource = queueName;
setQueryParam('Action','SendMessage');
setQueryParam('MessageBody', data);
setQueryParam('QueueUrl', queueName);
sendRequest();
}
}
public class ListQueueResponseMetadata {
public String RequestId;
}
public class ListQueuesResult {
public String[] queueUrls;
}
public class ListQueuesResponse {
public ListQueuesResult ListQueuesResult;
}
public class ListQueueResponse {
public ListQueuesResponse ListQueuesResponse;
public ListQueueResponseMetadata ResponseMetadata;
}
public static void sendMessage(String queueName, String message) {
SendMessageRequest req = new SendMessageRequest();
req.execute(queueName, message);
}
public static ListQueuesResult listQueues() {
return new ListQueuesRequest().execute();
}
}
@mftaher
Copy link

mftaher commented Feb 3, 2017

Trying to use the apex classes however, AWSSQS is not working as Variable not visible at Line 15. Can you help to make this work please? I'm trying to send SQS message from salesforce.

@Maxxpower
Copy link

Maxxpower commented Jun 28, 2017

Where can i put my account number? in the queuename? This format maybe? /accountnumber/queuename ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment