/************************************************************************* * (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * This file may incorporate work covered under the following copyright and permission notice: * * Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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. ************************************************************************/ package com.eucalyptus.simplequeue; import com.amazonaws.util.BinaryUtils; import com.amazonaws.util.Md5Utils; import com.eucalyptus.auth.AuthException; import com.eucalyptus.auth.AuthQuotaException; import com.eucalyptus.auth.Permissions; import com.eucalyptus.auth.PolicyParseException; import com.eucalyptus.auth.euare.Accounts; import com.eucalyptus.auth.euare.identity.region.RegionConfigurations; import com.eucalyptus.auth.policy.PolicyParser; import com.eucalyptus.auth.policy.ern.Ern; import com.eucalyptus.auth.principal.Principals; import com.eucalyptus.auth.principal.User; import com.eucalyptus.auth.type.LimitedType; import com.eucalyptus.cloudwatch.common.msgs.PutMetricDataType; import com.eucalyptus.component.ServiceUris; import com.eucalyptus.component.Topology; import com.eucalyptus.component.annotation.ComponentNamed; import com.eucalyptus.context.Context; import com.eucalyptus.context.Contexts; import com.eucalyptus.simplequeue.async.CloudWatchClient; import com.eucalyptus.simplequeue.async.NotifyClient; import com.eucalyptus.simplequeue.common.policy.SimpleQueuePolicySpec; import com.eucalyptus.simplequeue.config.SimpleQueueProperties; import com.eucalyptus.simplequeue.exceptions.AccessDeniedException; import com.eucalyptus.simplequeue.exceptions.BatchEntryIdsNotDistinctException; import com.eucalyptus.simplequeue.exceptions.EmptyBatchRequestException; import com.eucalyptus.simplequeue.exceptions.InternalFailureException; import com.eucalyptus.simplequeue.exceptions.InvalidAddressException; import com.eucalyptus.simplequeue.exceptions.InvalidAttributeNameException; import com.eucalyptus.simplequeue.exceptions.InvalidBatchEntryIdException; import com.eucalyptus.simplequeue.exceptions.InvalidParameterValueException; import com.eucalyptus.simplequeue.exceptions.LimitExceededException; import com.eucalyptus.simplequeue.exceptions.MissingParameterException; import com.eucalyptus.simplequeue.exceptions.QueueAlreadyExistsException; import com.eucalyptus.simplequeue.exceptions.QueueDoesNotExistException; import com.eucalyptus.simplequeue.exceptions.SimpleQueueException; import com.eucalyptus.simplequeue.exceptions.TooManyEntriesInBatchRequestException; import com.eucalyptus.simplequeue.exceptions.UnsupportedOperationException; import com.eucalyptus.simplequeue.persistence.PersistenceFactory; import com.eucalyptus.simplequeue.persistence.Queue; import com.eucalyptus.system.Threads; import com.eucalyptus.util.EucalyptusCloudException; import com.eucalyptus.util.Exceptions; import com.eucalyptus.util.RestrictedTypes; import com.eucalyptus.ws.Role; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Function; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableRangeSet; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; import com.google.common.collect.Sets; import net.sf.json.JSONException; import org.apache.log4j.Logger; import org.apache.xml.security.exceptions.Base64DecodingException; import org.apache.xml.security.utils.Base64; import javax.annotation.Nullable; import javax.persistence.PersistenceException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.URL; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Collectors; @ComponentNamed public class SimpleQueueService { private static final ScheduledExecutorService sendMessageNotificationsScheduledExecutorService = Executors .newScheduledThreadPool(4, Threads.threadFactory( "simplequeue-send-message-notification-%d" ) ); static final Logger LOG = Logger.getLogger(SimpleQueueService.class); private static int checkAttributeIntMinMax(Attribute attribute, int min, int max) throws InvalidParameterValueException { int value; try { value = Integer.parseInt(attribute.getValue()); } catch (Exception e) { throw new InvalidParameterValueException(attribute.getName() + " must be a number"); } if (value < min || value > max) { throw new InvalidParameterValueException(attribute.getName() + " must be a number " + "between " + min + " and " + max); } return value; } public CreateQueueResponseType createQueue(CreateQueueType request) throws SimpleQueueException { CreateQueueResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final String accountId = ctx.getAccountNumber(); if (!Permissions.isAuthorized(SimpleQueuePolicySpec.VENDOR_SIMPLEQUEUE, SimpleQueuePolicySpec.SIMPLEQUEUE_CREATEQUEUE, "", ctx.getAccount(), SimpleQueuePolicySpec.SIMPLEQUEUE_CREATEQUEUE, ctx.getAuthContext())) { throw new AccessDeniedException("Not authorized."); } if (request.getQueueName() == null) { throw new InvalidParameterValueException("Value for parameter QueueName is invalid. Reason: Must specify a queue name."); } if (request.getQueueName().isEmpty()) { throw new InvalidParameterValueException("Queue name cannot be empty."); } Pattern queueNamePattern = Pattern.compile("[A-Za-z0-9_-]+"); if (!queueNamePattern.matcher(request.getQueueName()).matches() || request.getQueueName().length() < 1 || request.getQueueName().length() > SimpleQueueProperties.MAX_QUEUE_NAME_LENGTH_CHARS) { throw new InvalidParameterValueException("Queue name can only include alphanumeric characters, hyphens, or " + "underscores. 1 to " + SimpleQueueProperties.MAX_QUEUE_NAME_LENGTH_CHARS + " in length"); } Map<String, String> attributeMap = Maps.newTreeMap(); // set some defaults (TODO: constants) attributeMap.put(Constants.DELAY_SECONDS, "0"); attributeMap.put(Constants.MAXIMUM_MESSAGE_SIZE, "262144"); attributeMap.put(Constants.MESSAGE_RETENTION_PERIOD, "345600"); attributeMap.put(Constants.RECEIVE_MESSAGE_WAIT_TIME_SECONDS, "0"); attributeMap.put(Constants.VISIBILITY_TIMEOUT, "30"); if (request.getAttribute() != null) { setAndValidateAttributes(accountId, request.getAttribute(), attributeMap); String nowSecs = "" + currentTimeSeconds(); attributeMap.put(Constants.CREATED_TIMESTAMP, nowSecs); attributeMap.put(Constants.LAST_MODIFIED_TIMESTAMP, nowSecs); // see if the queue already exists... // TODO: maybe record arn or queue url Queue queue = PersistenceFactory.getQueuePersistence().lookupQueue(accountId, request.getQueueName()); if (queue == null) { Supplier<Queue> allocator = new Supplier<Queue>() { @Override public Queue get() { try { return PersistenceFactory.getQueuePersistence().createQueue(accountId, request.getQueueName(), attributeMap); } catch (SimpleQueueException e) { throw Exceptions.toUndeclared( e ); } } }; try { queue = RestrictedTypes.allocateUnitlessResource(allocator); } catch (AuthQuotaException e) { throw new LimitExceededException(e.getMessage()); } } else { // make sure fields match Set<String> keysWeCareAbout = Sets.newHashSet( Constants.DELAY_SECONDS, Constants.MAXIMUM_MESSAGE_SIZE, Constants.MESSAGE_RETENTION_PERIOD, Constants.RECEIVE_MESSAGE_WAIT_TIME_SECONDS, Constants.VISIBILITY_TIMEOUT, Constants.POLICY, Constants.REDRIVE_POLICY); Map<String, String> requestAttributeMap = Maps.newTreeMap(); requestAttributeMap.putAll(attributeMap); Map<String, String> queueAttributeMap = Maps.newTreeMap(); queueAttributeMap.putAll(queue.getAttributes()); requestAttributeMap.keySet().retainAll(keysWeCareAbout); queueAttributeMap.keySet().retainAll(keysWeCareAbout); if (!Objects.equals(requestAttributeMap, queueAttributeMap)) { throw new QueueAlreadyExistsException(request.getQueueName() + " already exists."); } // TODO: determine if idempotency updates last modified time. } String queueUrl = getQueueUrlFromQueueUrlParts(new QueueUrlParts(accountId, request.getQueueName())); reply.getCreateQueueResult().setQueueUrl(queueUrl); } } catch (Exception ex) { handleException(ex); } return reply; } private static void setAndValidateAttributes(String accountId, Iterable<Attribute> requestAttributes, Map<String, String> attributeMap) throws SimpleQueueException { for (Attribute attribute : requestAttributes) { switch (attribute.getName()) { case Constants.DELAY_SECONDS: checkAttributeIntMinMax(attribute, 0, SimpleQueueProperties.MAX_DELAY_SECONDS); attributeMap.put(attribute.getName(), attribute.getValue()); break; case Constants.MAXIMUM_MESSAGE_SIZE: checkAttributeIntMinMax(attribute, 1024, SimpleQueueProperties.MAX_MAXIMUM_MESSAGE_SIZE); attributeMap.put(attribute.getName(), attribute.getValue()); break; case Constants.MESSAGE_RETENTION_PERIOD: checkAttributeIntMinMax(attribute, 60, SimpleQueueProperties.MAX_MESSAGE_RETENTION_PERIOD); attributeMap.put(attribute.getName(), attribute.getValue()); break; case Constants.RECEIVE_MESSAGE_WAIT_TIME_SECONDS: checkAttributeIntMinMax(attribute, 0, SimpleQueueProperties.MAX_RECEIVE_MESSAGE_WAIT_TIME_SECONDS); attributeMap.put(attribute.getName(), attribute.getValue()); break; case Constants.VISIBILITY_TIMEOUT: checkAttributeIntMinMax(attribute, 0, SimpleQueueProperties.MAX_VISIBILITY_TIMEOUT); attributeMap.put(attribute.getName(), attribute.getValue()); break; case Constants.POLICY: if (Strings.isNullOrEmpty(attribute.getValue())) { attributeMap.remove(attribute.getName()); continue; } // TODO: we don't support wildcard Principal try { minimallyCheckPolicy(attribute.getValue()); PolicyParser.getResourceInstance().parse(attribute.getValue()); } catch (PolicyParseException | IOException e) { throw new InvalidParameterValueException("Invalid value for the parameter Policy. "); } attributeMap.put(attribute.getName(), attribute.getValue()); break; case Constants.REDRIVE_POLICY: if (Strings.isNullOrEmpty(attribute.getValue())) { attributeMap.remove(attribute.getName()); continue; } // TODO: maybe put this json stuff in its own class/method JsonNode redrivePolicyJsonNode; try { redrivePolicyJsonNode = new ObjectMapper().readTree(attribute.getValue()); } catch (IOException e) { throw new InvalidParameterValueException("Invalid value for the parameter RedrivePolicy. Reason: Redrive policy is not a valid JSON map."); } if (redrivePolicyJsonNode == null || !redrivePolicyJsonNode.isObject()) { throw new InvalidParameterValueException("Invalid value for the parameter RedrivePolicy. Reason: Redrive policy is not a valid JSON map."); } if (!redrivePolicyJsonNode.has(Constants.MAX_RECEIVE_COUNT)) { throw new InvalidParameterValueException("Value " + attribute.getValue() + " for parameter " + "RedrivePolicy is invalid. Reason: Redrive policy does not contain mandatory attribute: " + Constants.MAX_RECEIVE_COUNT + "."); } if (!redrivePolicyJsonNode.has(Constants.DEAD_LETTER_TARGET_ARN)) { throw new InvalidParameterValueException("Value " + attribute.getValue() + " for parameter " + "RedrivePolicy is invalid. Reason: Redrive policy does not contain mandatory attribute: " + Constants.DEAD_LETTER_TARGET_ARN + "."); } if (redrivePolicyJsonNode.size() > 2) { throw new InvalidParameterValueException("Value " + attribute.getValue() + " for parameter " + "RedrivePolicy is invalid. Reason: Only following attributes are supported: [" + Constants.DEAD_LETTER_TARGET_ARN + ", " + Constants.MAX_RECEIVE_COUNT + "]."); } JsonNode maxReceiveCountJsonNode = redrivePolicyJsonNode.get(Constants.MAX_RECEIVE_COUNT); // note, if node is non-textual or has non-integer value, .asInt() will return 0, which is ok here. if (maxReceiveCountJsonNode == null || (maxReceiveCountJsonNode.asInt() < 1) || (maxReceiveCountJsonNode.asInt() > SimpleQueueProperties.MAX_MAX_RECEIVE_COUNT)) { throw new InvalidParameterValueException("Value " + attribute.getValue() + " for parameter " + "RedrivePolicy is invalid. Reason: Invalid value for " + Constants.MAX_RECEIVE_COUNT + ": " + maxReceiveCountJsonNode + ", valid values are from 1 to" + SimpleQueueProperties.MAX_MAX_RECEIVE_COUNT + " both " + "inclusive."); } JsonNode deadLetterTargetArnJsonNode = redrivePolicyJsonNode.get(Constants.DEAD_LETTER_TARGET_ARN); if (deadLetterTargetArnJsonNode == null || !(deadLetterTargetArnJsonNode.isTextual())) { throw new InvalidParameterValueException("Value " + attribute.getValue() + " for parameter " + "RedrivePolicy is invalid. Reason: Invalid value for " + Constants.DEAD_LETTER_TARGET_ARN + "."); } Ern simpleQueueArn; try { simpleQueueArn = Ern.parse(deadLetterTargetArnJsonNode.textValue()); } catch (JSONException e) { throw new InvalidParameterValueException("Value " + attribute.getValue() + " for parameter " + "RedrivePolicy is invalid. Reason: Invalid value for " + Constants.DEAD_LETTER_TARGET_ARN + "."); } if (!simpleQueueArn.getRegion().equals(RegionConfigurations.getRegionNameOrDefault())) { throw new InvalidParameterValueException("Value " + attribute.getValue() + " for parameter " + "RedrivePolicy is invalid. Reason: Dead-letter target must be in same region as the source."); } if (!simpleQueueArn.getAccount().equals(accountId)) { throw new InvalidParameterValueException("Value " + attribute.getValue() + " for parameter " + "RedrivePolicy is invalid. Reason: Dead-letter target owner should be same as the source."); } if (PersistenceFactory.getQueuePersistence().lookupQueue(simpleQueueArn.getAccount(), simpleQueueArn.getResourceName()) == null) { throw new InvalidParameterValueException("Value " + attribute.getValue() + " for parameter " + "RedrivePolicy is invalid. Reason: Dead letter target does not exist."); } attributeMap.put(attribute.getName(), attribute.getValue()); break; default: throw new InvalidAttributeNameException("Unknown Attribute " + attribute.getName()); } } } private static void minimallyCheckPolicy(String policyJson) throws IOException { // check valid json JsonNode jsonNode = new ObjectMapper().readTree(policyJson); if (!jsonNode.isObject()) { throw new IOException("Policy is not a JSON object"); } if (!jsonNode.has("Statement") || !(jsonNode.get("Statement").isObject() || jsonNode.get("Statement").isArray())) { throw new IOException("Policy requires at least one Statement, which is a JSON object"); } if (jsonNode.get("Statement").isArray()) { if (jsonNode.get("Statement").size() < 1) { throw new IOException("Policy requires at least one Statement, which is a JSON object"); } else { for (JsonNode statementNode: Lists.newArrayList(jsonNode.get("Statement").elements())) { if (!statementNode.isObject()) { throw new IOException("Each Statement must be a JSON object"); } } } } } private static String getQueueUrlFromQueueUrlParts(QueueUrlParts queueUrlParts) { return ServiceUris.remotePublicify(Topology.lookup(SimpleQueue.class)).toString() + queueUrlParts.getAccountId() + "/" + queueUrlParts.getQueueName(); } private static class QueueUrlParts { private String accountId; private String queueName; private QueueUrlParts() { } private QueueUrlParts(String accountId, String queueName) { this.accountId = accountId; this.queueName = queueName; } public String getAccountId() { return accountId; } public void setAccountId(String accountId) { this.accountId = accountId; } public String getQueueName() { return queueName; } public void setQueueName(String queueName) { this.queueName = queueName; } } interface QueueUrlPartsParser { boolean matches(URL queueUrl); QueueUrlParts getQueueUrlParts(URL queueUrl); } private static Collection<QueueUrlPartsParser> queueUrlPartsParsers = Lists.newArrayList( new AccountIdAndQueueNamePartsParser(), new ServicePathAccountIdAndQueueNamePartsParser() ); private static class AccountIdAndQueueNamePartsParser implements QueueUrlPartsParser { @Override public boolean matches(URL queueUrl) { if (queueUrl != null && queueUrl.getPath() != null) { return (Splitter.on('/').omitEmptyStrings().splitToList(queueUrl.getPath()).size() == 2); } else { return false; } } @Override public QueueUrlParts getQueueUrlParts(URL queueUrl) { // TODO: we are duplicating Splitter code here. consider refactoring if too slow. List<String> pathParts = Splitter.on('/').omitEmptyStrings().splitToList(queueUrl.getPath()); QueueUrlParts queueUrlParts = new QueueUrlParts(); queueUrlParts.setAccountId(pathParts.get(0)); queueUrlParts.setQueueName(pathParts.get(1)); return queueUrlParts; } } private static class ServicePathAccountIdAndQueueNamePartsParser implements QueueUrlPartsParser { @Override public boolean matches(URL queueUrl) { if (queueUrl != null && queueUrl.getPath() != null) { List<String> pathParts = Splitter.on('/').omitEmptyStrings().splitToList(queueUrl.getPath()); return (pathParts != null && pathParts.size() == 4 && "services".equals(pathParts.get(0)) && "simplequeue".equals(pathParts.get(1))); } else { return false; } } @Override public QueueUrlParts getQueueUrlParts(URL queueUrl) { // TODO: we are duplicating Splitter code here. consider refactoring if too slow. List<String> pathParts = Splitter.on('/').omitEmptyStrings().splitToList(queueUrl.getPath()); QueueUrlParts queueUrlParts = new QueueUrlParts(); queueUrlParts.setAccountId(pathParts.get(2)); queueUrlParts.setQueueName(pathParts.get(3)); return queueUrlParts; } } private static QueueUrlParts getQueueUrlParts(String queueUrlStr) throws InvalidAddressException { QueueUrlParts queueUrlParts = null; try { URL queueUrl = new URL(queueUrlStr); for (QueueUrlPartsParser queueUrlPartsParser: queueUrlPartsParsers) { if (queueUrlPartsParser.matches(queueUrl)) { queueUrlParts = queueUrlPartsParser.getQueueUrlParts(queueUrl); break; } } // validate account id Accounts.lookupAccountById(queueUrlParts.getAccountId()).getAccountNumber(); } catch (MalformedURLException | NullPointerException | AuthException e) { queueUrlParts = null; } if (queueUrlParts == null) { throw new InvalidAddressException("The address " + queueUrlStr + " is not valid for this endpoint."); } return queueUrlParts; } public GetQueueUrlResponseType getQueueUrl(GetQueueUrlType request) throws EucalyptusCloudException { GetQueueUrlResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final String accountId = request.getQueueOwnerAWSAccountId() != null ? request.getQueueOwnerAWSAccountId() : ctx.getAccountNumber(); String queueUrl = getQueueUrlFromQueueUrlParts(new QueueUrlParts(accountId, request.getQueueName())); try { Queue queue = getAndCheckPermissionOnQueue(queueUrl); reply.getGetQueueUrlResult().setQueueUrl(queueUrl); } catch (AccessDeniedException ex) { // This is an example to comply with AWS. Get queue url doesn't return "AccessDenied" throw new QueueDoesNotExistException("The specified queue does not exist."); } } catch (Exception ex) { handleException(ex); } return reply; } public ListQueuesResponseType listQueues(ListQueuesType request) throws EucalyptusCloudException { ListQueuesResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final String accountId = ctx.getAccountNumber(); if (!Permissions.isAuthorized(SimpleQueuePolicySpec.VENDOR_SIMPLEQUEUE, SimpleQueuePolicySpec.SIMPLEQUEUE_LISTQUEUES, "", ctx.getAccount(), SimpleQueuePolicySpec.SIMPLEQUEUE_LISTQUEUES, ctx.getAuthContext())) { throw new AccessDeniedException("Not authorized."); } Collection<Queue.Key> queueKeys; if (ctx.isAdministrator() && "verbose".equals(request.getQueueNamePrefix())) { queueKeys = PersistenceFactory.getQueuePersistence().listQueues(null, null); } else queueKeys = PersistenceFactory.getQueuePersistence().listQueues(accountId, request.getQueueNamePrefix()); if (queueKeys != null) { for (Queue.Key queueKey: queueKeys) { reply.getListQueuesResult().getQueueUrl().add(getQueueUrlFromQueueUrlParts(new QueueUrlParts(queueKey.getAccountId(), queueKey.getQueueName()))); } } } catch (Exception ex) { handleException(ex); } return reply; } public AddPermissionResponseType addPermission(AddPermissionType request) throws EucalyptusCloudException { AddPermissionResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); String queueArn = queue.getArn(); ArrayList<String> principalIds = Lists.newArrayList(); if (request.getAwsAccountId() == null || request.getAwsAccountId().isEmpty()) { // Note: this is the exact message AWS uses. throw new MissingParameterException("The request must contain the parameter PrincipalId."); } for (String awsAccountId: request.getAwsAccountId()) { // oddly AWS will fail if all principal ids are invalid but if even one isn't, it won't fail. However, // it will only add valid ids. try { Accounts.lookupAccountById(awsAccountId).getAccountNumber(); principalIds.add("arn:aws:iam::" + awsAccountId + ":root"); } catch (AuthException ignore) { } } if (principalIds.isEmpty()) { throw new InvalidParameterValueException("Value " + request.getAwsAccountId() + " for parameter PrincipalId is invalid. Reason: Unable to verify."); } ArrayList<String> actionNames = Lists.newArrayList(); if (request.getActionName() == null || request.getActionName().isEmpty()) { throw new MissingParameterException("The request must contain the parameter Actions."); } Set<String> validActionNames = Sets.newHashSet( "*", "SendMessage", "ReceiveMessage", "DeleteMessage", "ChangeMessageVisibility", "GetQueueAttributes", "GetQueueUrl", "ListDeadLetterSourceQueues", "PurgeQueue" ); Set<String> onlyOwnerActionNames = Sets.newHashSet( "AddPermission", "CreateQueue", "DeleteQueue", "ListQueues", "SetQueueAttributes", "RemovePermission" ); for (String actionName: request.getActionName()) { if (validActionNames.contains(actionName)) { actionNames.add("SQS:" + actionName); } else if (onlyOwnerActionNames.contains(actionName)) { throw new InvalidParameterValueException("Value SQS:" + actionName + " for parameter ActionName is invalid. Reason: Only the queue owner is allowed to invoke this action."); } else { throw new InvalidParameterValueException("Value SQS:" + actionName + " for parameter ActionName is invalid. Reason: Please refer to the appropriate WSDL for a list of valid actions."); } } if (request.getLabel() == null) { throw new InvalidParameterValueException("Value for parameter Label is invalid. Reason: Must specify a label."); } if (request.getLabel().isEmpty()) { throw new InvalidParameterValueException("Label cannot be empty."); } Pattern labelPattern = Pattern.compile("[A-Za-z0-9_-]+"); if (!labelPattern.matcher(request.getLabel()).matches() || request.getLabel().length() < 1 || request.getLabel().length() > SimpleQueueProperties.MAX_LABEL_LENGTH_CHARS) { throw new InvalidParameterValueException("Label can only include alphanumeric characters, hyphens, or " + "underscores. 1 to " + SimpleQueueProperties.MAX_LABEL_LENGTH_CHARS + " in length"); } String policy = queue.getPolicyAsString(); if (policy == null || policy.isEmpty()) { // new policy ObjectNode policyNode = new ObjectMapper().createObjectNode(); policyNode.put("Version", "2008-10-17"); policyNode.put("Id", queueArn + "/SQSDefaultPolicy"); ArrayNode statementArrayNode = policyNode.putArray("Statement"); addStatementToPolicy(request.getLabel(), principalIds, actionNames, queueArn, statementArrayNode); policy = policyNode.toString(); } else { ObjectNode policyNode = null; try { policyNode = (ObjectNode) new ObjectMapper().readTree(queue.getPolicyAsString()); if (!policyNode.has("Statement") || !policyNode.get("Statement").isContainerNode()) { throw new IOException("Invalid existing policy"); } if (policyNode.get("Statement").isObject()) { ObjectNode statementNodeIndividual = (ObjectNode) policyNode.get("Statement"); policyNode.remove("Statement"); ArrayNode statementArrayNode = policyNode.putArray("Statement"); statementArrayNode.add(statementNodeIndividual); } for (JsonNode statementNode: Lists.newArrayList(policyNode.get("Statement").elements())) { if (!statementNode.isObject()) { throw new IOException("Invalid existing policy"); } if (statementNode.has("Sid") && !statementNode.get("Sid").isTextual()) { throw new IOException("Invalid existing policy"); } if (statementNode.has("Sid") && request.getLabel().equals(statementNode.get("Sid").textValue())) { throw new InvalidParameterValueException(request.getLabel() + " already used as an Sid in the Queue Policy"); } } addStatementToPolicy(request.getLabel(), principalIds, actionNames, queueArn, (ArrayNode) policyNode.get("Statement")); policy = policyNode.toString(); } catch (ClassCastException | IOException e) { throw new InternalFailureException("Invalid existing queue policy"); } } Map<String, String> existingAttributes = queue.getAttributes(); setAndValidateAttributes(queue.getAccountId(), Collections.singletonList(new Attribute(Constants.POLICY, policy)), existingAttributes); existingAttributes.put(Constants.LAST_MODIFIED_TIMESTAMP, String.valueOf(currentTimeSeconds())); PersistenceFactory.getQueuePersistence().updateQueueAttributes(queue.getAccountId(), queue.getQueueName(), existingAttributes); } catch (Exception ex) { handleException(ex); } return reply; } private static void addStatementToPolicy(String label, Collection<String> principalIds, Collection<String> actionNames, String resourceId, ArrayNode statementArrayNode) { ObjectNode statementNode = statementArrayNode.addObject(); statementNode.put("Sid", label); statementNode.put("Effect","Allow"); ObjectNode principalNode = statementNode.putObject("Principal"); if (principalIds.size() == 1) { principalNode.put("AWS", principalIds.iterator().next()); } else { ArrayNode awsNode = principalNode.putArray("AWS"); for (String principalId: principalIds) { awsNode.add(principalId); } } if (actionNames.size() == 1) { statementNode.put("Action", actionNames.iterator().next()); } else { ArrayNode actionNode = statementNode.putArray("Action"); for (String actionName: actionNames) { actionNode.add(actionName); } } statementNode.put("Resource", resourceId); } public ChangeMessageVisibilityResponseType changeMessageVisibility(ChangeMessageVisibilityType request) throws EucalyptusCloudException { ChangeMessageVisibilityResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); final Integer visibilityTimeout = request.getVisibilityTimeout(); final String receiptHandle = request.getReceiptHandle(); QueueUrlParts queueUrlParts = getQueueUrlParts(request.getQueueUrl()); handleChangeMessageVisibility(visibilityTimeout, receiptHandle, queue); } catch (Exception ex) { handleException(ex); } return reply; } private static void handleChangeMessageVisibility(Integer visibilityTimeout, String receiptHandle, Queue queue) throws SimpleQueueException { if (visibilityTimeout == null) { throw new MissingParameterException("VisibilityTimeout is a required field"); } if (visibilityTimeout < 0 || visibilityTimeout > SimpleQueueProperties.MAX_VISIBILITY_TIMEOUT) { throw new InvalidParameterValueException("VisibilityTimeout must be between 0 and " + SimpleQueueProperties.MAX_VISIBILITY_TIMEOUT); } if (receiptHandle == null) { throw new MissingParameterException("ReceiptHandle is a required field"); } PersistenceFactory.getMessagePersistence().changeMessageVisibility(queue.getKey(), receiptHandle, visibilityTimeout); } public DeleteMessageResponseType deleteMessage(DeleteMessageType request) throws EucalyptusCloudException { DeleteMessageResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); String receiptHandle = request.getReceiptHandle(); if (PersistenceFactory.getMessagePersistence().deleteMessage(queue.getKey(), receiptHandle)) { if (SimpleQueueProperties.ENABLE_METRICS_COLLECTION) { PutMetricDataType putMetricDataType = CloudWatchClient.getSQSPutMetricDataType(queue.getKey()); CloudWatchClient.addSQSMetricDatum(putMetricDataType, queue.getKey(), new Date(), Constants.NUMBER_OF_MESSAGES_DELETED, 1.0, "Count"); CloudWatchClient.putMetricData(putMetricDataType); } } } catch (Exception ex) { handleException(ex); } return reply; } public DeleteQueueResponseType deleteQueue(DeleteQueueType request) throws EucalyptusCloudException { DeleteQueueResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); PersistenceFactory.getMessagePersistence().deleteAllMessages(queue.getKey()); PersistenceFactory.getQueuePersistence().deleteQueue(queue.getAccountId(), queue.getQueueName()); } catch (Exception ex) { handleException(ex); } return reply; } private static Queue getAndCheckPermissionOnQueue(String queueUrl) throws QueueDoesNotExistException, AccessDeniedException, InvalidAddressException, InternalFailureException { try { final QueueUrlParts queueUrlParts = getQueueUrlParts( queueUrl ); final Queue queue = PersistenceFactory.getQueuePersistence( ) .lookupQueue( queueUrlParts.getAccountId( ), queueUrlParts.getQueueName( ) ); // some actions support inter-account access, so authorize accordingly boolean actionIsASharedQueueAction = SimpleQueueMetadata.sharedQueueActions().contains(RestrictedTypes.getIamActionByMessageType()); boolean requestAccountMatchesQueueAccount = queueUrlParts.getAccountId().equals(Contexts.lookup().getAccountNumber()); if (queue == null) { if (requestAccountMatchesQueueAccount) { throw new QueueDoesNotExistException("The specified queue does not exist."); } else if (actionIsASharedQueueAction) { throw new QueueDoesNotExistException("The specified queue does not exist or you do not have access to it."); } else { throw new AccessDeniedException("Not authorized."); } } final QueueResolver resolver = new QueueResolver( queue ); return RestrictedTypes.doPrivileged( queue.getDisplayName( ), resolver ); } catch (AuthException ex) { throw new AccessDeniedException("Not authorized."); } catch (NoSuchElementException ex) { throw new QueueDoesNotExistException("The specified queue does not exist."); } catch (PersistenceException ex) { throw new InternalFailureException(ex.getMessage()); } } public PurgeQueueResponseType purgeQueue(PurgeQueueType request) throws EucalyptusCloudException { PurgeQueueResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); PersistenceFactory.getMessagePersistence().deleteAllMessages(queue.getKey()); } catch (Exception ex) { handleException(ex); } return reply; } public GetQueueAttributesResponseType getQueueAttributes(GetQueueAttributesType request) throws EucalyptusCloudException { GetQueueAttributesResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); Map<String, String> attributes = Maps.newHashMap(); if (queue.getAttributes() != null) { attributes.putAll(queue.getAttributes()); } attributes.putAll(PersistenceFactory.getMessagePersistence().getApproximateMessageCounts(queue.getKey())); attributes.put(Constants.QUEUE_ARN, queue.getArn()); Set<String> validAttributes = ImmutableSet.of( Constants.ALL, Constants.APPROXIMATE_NUMBER_OF_MESSAGES, Constants.APPROXIMATE_NUMBER_OF_MESSAGES_NOT_VISIBLE, Constants.VISIBILITY_TIMEOUT, Constants.CREATED_TIMESTAMP, Constants.LAST_MODIFIED_TIMESTAMP, Constants.POLICY, Constants.MAXIMUM_MESSAGE_SIZE, Constants.MESSAGE_RETENTION_PERIOD, Constants.QUEUE_ARN, Constants.APPROXIMATE_NUMBER_OF_MESSAGES_DELAYED, Constants.DELAY_SECONDS, Constants.RECEIVE_MESSAGE_WAIT_TIME_SECONDS, Constants.REDRIVE_POLICY); Set<String> passedInAttributes = Sets.newHashSet(); if (request.getAttributeName() != null) { for (String passedInAttribute: request.getAttributeName()) { if (!validAttributes.contains(passedInAttribute)) { throw new InvalidAttributeNameException("Invalid attribute " + passedInAttribute); } passedInAttributes.add(passedInAttribute); } } // filter if (!passedInAttributes.contains(Constants.ALL)) { attributes.keySet().retainAll(passedInAttributes); } for (Map.Entry<String, String> attributeEntry: attributes.entrySet()) { reply.getGetQueueAttributesResult().getAttribute().add(new Attribute(attributeEntry.getKey(), attributeEntry.getValue())); } } catch (Exception ex) { handleException(ex); } return reply; } public RemovePermissionResponseType removePermission(RemovePermissionType request) throws EucalyptusCloudException { RemovePermissionResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); if (request.getLabel() == null) { throw new InvalidParameterValueException("Value for parameter Label is invalid. Reason: Must specify a label."); } if (request.getLabel().isEmpty()) { throw new InvalidParameterValueException("Label cannot be empty."); } Pattern labelPattern = Pattern.compile("[A-Za-z0-9_-]+"); if (!labelPattern.matcher(request.getLabel()).matches() || request.getLabel().length() < 1 || request.getLabel().length() > SimpleQueueProperties.MAX_LABEL_LENGTH_CHARS) { throw new InvalidParameterValueException("Label can only include alphanumeric characters, hyphens, or " + "underscores. 1 to " + SimpleQueueProperties.MAX_LABEL_LENGTH_CHARS + " in length"); } String policy = queue.getPolicyAsString(); if (policy == null || policy.isEmpty()) { throw new InvalidParameterValueException("Value " + request.getLabel() + " for parameter Label is invalid. Reason: can't find label."); } ObjectNode policyNode = null; try { policyNode = (ObjectNode) new ObjectMapper().readTree(queue.getPolicyAsString()); if (!policyNode.has("Statement") || !policyNode.get("Statement").isContainerNode()) { throw new IOException("Invalid existing policy"); } if (policyNode.get("Statement").isObject()) { ObjectNode statementNodeIndividual = (ObjectNode) policyNode.get("Statement"); policyNode.remove("Statement"); ArrayNode statementArrayNode = policyNode.putArray("Statement"); statementArrayNode.add(statementNodeIndividual); } ArrayNode statementArrayNode = (ArrayNode) policyNode.get("Statement"); boolean foundLabel = false; for (int i = 0; i < statementArrayNode.size(); i++) { JsonNode statementNode = statementArrayNode.get(i); if (!statementNode.isObject()) { throw new IOException("Invalid existing policy"); } if (statementNode.has("Sid") && !statementNode.get("Sid").isTextual()) { throw new IOException("Invalid existing policy"); } if (statementNode.has("Sid") && request.getLabel().equals(statementNode.get("Sid").textValue())) { statementArrayNode.remove(i); i--; foundLabel = true; } } if (!foundLabel) { throw new IOException("didn't find label"); } if (statementArrayNode.size() == 0) { policy = ""; } else { policy = policyNode.toString(); } } catch (ClassCastException | IOException e) { throw new InvalidParameterValueException("Value " + request.getLabel() + " for parameter Label is invalid. Reason: can't find label."); } Map<String, String> existingAttributes = queue.getAttributes(); setAndValidateAttributes(queue.getAccountId(), Collections.singletonList(new Attribute(Constants.POLICY, policy)), existingAttributes); existingAttributes.put(Constants.LAST_MODIFIED_TIMESTAMP, String.valueOf(currentTimeSeconds())); PersistenceFactory.getQueuePersistence().updateQueueAttributes(queue.getAccountId(), queue.getQueueName(), existingAttributes); } catch (Exception ex) { handleException(ex); } return reply; } public ReceiveMessageResponseType receiveMessage(ReceiveMessageType request) throws EucalyptusCloudException { ReceiveMessageResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); final Map<String, String> receiveAttributes = Maps.newHashMap(); if (request.getVisibilityTimeout() != null) { if (request.getVisibilityTimeout() < 0 || request.getVisibilityTimeout() > SimpleQueueProperties.MAX_VISIBILITY_TIMEOUT) { throw new InvalidParameterValueException("VisibilityTimeout must be between 0 and " + SimpleQueueProperties.MAX_VISIBILITY_TIMEOUT); } receiveAttributes.put(Constants.VISIBILITY_TIMEOUT, "" + request.getVisibilityTimeout()); } if (request.getWaitTimeSeconds() != null) { if (request.getWaitTimeSeconds() < 0 || request.getWaitTimeSeconds() > SimpleQueueProperties.MAX_RECEIVE_MESSAGE_WAIT_TIME_SECONDS) { throw new InvalidParameterValueException("WaitTimeSeconds must be between 0 and " + SimpleQueueProperties.MAX_RECEIVE_MESSAGE_WAIT_TIME_SECONDS); } receiveAttributes.put(Constants.WAIT_TIME_SECONDS, "" + request.getWaitTimeSeconds()); } int maxNumberOfMessages = 1; if (request.getMaxNumberOfMessages() != null) { if (request.getMaxNumberOfMessages() < 1 || request.getMaxNumberOfMessages() > SimpleQueueProperties.MAX_RECEIVE_MESSAGE_MAX_NUMBER_OF_MESSAGES) { throw new InvalidParameterValueException("WaitTimeSeconds must be between 1 and " + SimpleQueueProperties.MAX_RECEIVE_MESSAGE_MAX_NUMBER_OF_MESSAGES); } maxNumberOfMessages = request.getMaxNumberOfMessages(); } receiveAttributes.put(Constants.MAX_NUMBER_OF_MESSAGES, "" + maxNumberOfMessages); boolean hasActiveLegalRedrivePolicy = false; Queue deadLetterQueue = null; String deadLetterTargetArn = null; int maxReceiveCount = 0; try { if (queue.getRedrivePolicy() != null && queue.getRedrivePolicy().isObject()) { deadLetterTargetArn = queue.getRedrivePolicy().get(Constants.DEAD_LETTER_TARGET_ARN).textValue(); Ern deadLetterQueueErn = Ern.parse(deadLetterTargetArn); maxReceiveCount = queue.getRedrivePolicy().get(Constants.MAX_RECEIVE_COUNT).asInt(); deadLetterQueue = PersistenceFactory.getQueuePersistence().lookupQueue(deadLetterQueueErn.getAccount(), deadLetterQueueErn.getResourceName()); hasActiveLegalRedrivePolicy = (deadLetterQueue != null && maxReceiveCount > 0); } } catch (Exception ignore) { // malformed or nonexistent redrive policy, just leave the message where it is? } if (deadLetterQueue != null) { receiveAttributes.put(Constants.DEAD_LETTER_TARGET_ARN, deadLetterTargetArn); receiveAttributes.put(Constants.MESSAGE_RETENTION_PERIOD, ""+deadLetterQueue.getMessageRetentionPeriod()); receiveAttributes.put(Constants.MAX_RECEIVE_COUNT, ""+maxReceiveCount); } int waitTimeSeconds = request.getWaitTimeSeconds() != null ? request.getWaitTimeSeconds() : queue.getReceiveMessageWaitTimeSeconds(); Collection<Message> messages = PersistenceFactory.getMessagePersistence().receiveMessages(queue, receiveAttributes); if (messages != null && !messages.isEmpty()) { sendReceivedMessagesCW(queue, messages, request.getAttributeName(), request.getMessageAttributeName()); reply.getReceiveMessageResult().getMessage().addAll(messages); } else { if (SimpleQueueProperties.ENABLE_LONG_POLLING && waitTimeSeconds > 0) { handleQueuePollingForReceive(queue, reply, new Callable<ReceiveMessageResult>() { @Override public ReceiveMessageResult call() throws Exception { Collection<Message> messages = PersistenceFactory.getMessagePersistence().receiveMessages(queue, receiveAttributes); if (messages != null && !messages.isEmpty()) { sendReceivedMessagesCW(queue, messages, request.getAttributeName(), request.getMessageAttributeName()); ReceiveMessageResult receiveMessageResult = new ReceiveMessageResult(); receiveMessageResult.getMessage().addAll(messages); return receiveMessageResult; } else { return null; } } }, System.currentTimeMillis()+waitTimeSeconds*1000L ); return null; } else { sendEmptyReceiveCW(queue); } } } catch (Exception ex) { handleException(ex); } return reply; } private static void sendReceivedMessagesCW(Queue queue, Collection<Message> messages, ArrayList<String> attributeNames, ArrayList<String> messageAttributeNames) throws EucalyptusCloudException, AuthException { Date now = new Date(); for (Message message: messages) { filterReceiveAttributes(message, attributeNames); filterReceiveMessageAttributes(message, messageAttributeNames); } if (SimpleQueueProperties.ENABLE_METRICS_COLLECTION) { PutMetricDataType putMetricDataType = CloudWatchClient.getSQSPutMetricDataType(queue.getKey()); CloudWatchClient.addSQSMetricDatum(putMetricDataType, queue.getKey(), now, Constants.NUMBER_OF_MESSAGES_RECEIVED, messages.size(), 1.0, 1.0, messages.size(), "Count"); CloudWatchClient.putMetricData(putMetricDataType); } } static void sendEmptyReceiveCW(Queue queue) throws AuthException { if (SimpleQueueProperties.ENABLE_METRICS_COLLECTION) { PutMetricDataType putMetricDataType = CloudWatchClient.getSQSPutMetricDataType(queue.getKey()); CloudWatchClient.addSQSMetricDatum(putMetricDataType, queue.getKey(), new Date(), Constants.NUMBER_OF_EMPTY_RECEIVES, 1.0, "Count"); CloudWatchClient.putMetricData(putMetricDataType); } } private static void filterReceiveMessageAttributes(Message message, ArrayList<String> matchingMessageAttributeNames) throws EucalyptusCloudException { if (message.getMessageAttribute() != null) { boolean changed = true; Iterator<MessageAttribute> iter = message.getMessageAttribute().iterator(); while (iter.hasNext()) { MessageAttribute messageAttribute = iter.next(); boolean keepMessageAttribute = false; if (matchingMessageAttributeNames != null) { for (String matchingMessageAttributeName: matchingMessageAttributeNames) { // specific matches are exact, All, literal .*, or Prefix.* if (matchingMessageAttributeName.equals(messageAttribute.getName()) || matchingMessageAttributeName.equals(Constants.ALL) || matchingMessageAttributeName.equals(".*")) { keepMessageAttribute = true; break; } // check prefix match if (matchingMessageAttributeName.endsWith(".*")) { String prefix = matchingMessageAttributeName.substring(0, matchingMessageAttributeName.length() - 2); if (messageAttribute.getName().startsWith(prefix)) { keepMessageAttribute = true; break; } } } } if (!keepMessageAttribute) { changed = true; iter.remove(); } } if (changed) { message.setmD5OfMessageAttributes(calculateMessageAttributesMd5(convertMessageAttributesToMap(message.getMessageAttribute()))); } } } private static void filterReceiveAttributes(Message message, ArrayList<String> matchingAttributeNames) { if (message.getAttribute() != null) { Iterator<Attribute> iter = message.getAttribute().iterator(); while (iter.hasNext()) { Attribute attribute = iter.next(); // we only keep attributes that match the attribute name set (exact match or All.) if ((matchingAttributeNames == null || !(matchingAttributeNames.contains(Constants.ALL) || matchingAttributeNames.contains(attribute.getName())))) { iter.remove(); } } } } private static int validateMessageAttributeNameAndCalculateLength(String name, Collection<String> previousNames) throws InvalidParameterValueException { if (Strings.isNullOrEmpty(name)) { throw new InvalidParameterValueException("Message attribute name can not be null or empty"); } if (name.length() > SimpleQueueProperties.MAX_MESSAGE_ATTRIBUTE_NAME_LENGTH) { throw new InvalidParameterValueException("Message attribute name can not be longer than " + SimpleQueueProperties.MAX_MESSAGE_ATTRIBUTE_NAME_LENGTH + " characters"); } if (name.toLowerCase().startsWith("amazon") || name.toLowerCase().startsWith("aws")) { throw new InvalidParameterValueException("Message attribute names starting with 'AWS.' or 'Amazon.' are reserved for use by Amazon."); } if (name.contains("..")) { throw new InvalidParameterValueException("Message attribute name can not have successive '.' characters. "); } for (int codePoint : name.codePoints().toArray()) { if (!validMessageNameCodePoints.contains(codePoint)) { throw new InvalidParameterValueException("Invalid non-alphanumeric character '#x" + Integer.toHexString(codePoint) + "' was found in the message attribute name. Can only include alphanumeric characters, hyphens, underscores, or dots."); } } if (previousNames.contains(name)) { throw new InvalidParameterValueException("Message attribute name '" + name + "' already exists."); } previousNames.add(name); return name.getBytes(UTF8).length; } private static int validateMessageAttributeValueAndCalculateLength(MessageAttributeValue value, String name) throws com.eucalyptus.simplequeue.exceptions.UnsupportedOperationException, InvalidParameterValueException { int attributeValueLength = 0; if (value == null) { throw new InvalidParameterValueException("The message attribute '" + name + "' must contain non-empty message attribute value."); } String type = value.getDataType(); if (Strings.isNullOrEmpty(type)) { throw new InvalidParameterValueException("The message attribute '" + name + "' must contain non-empty message attribute type."); } boolean isStringType = type.equals("String") || type.startsWith("String."); boolean isBinaryType = type.equals("Binary") || type.startsWith("Binary."); boolean isNumberType = type.equals("Number") || type.startsWith("Number."); if (!isStringType && !isBinaryType && !isNumberType) { throw new InvalidParameterValueException("The message attribute '" + name +"' has an invalid message attribute type, the set of supported type prefixes is Binary, Number, and String."); } // this is done in .getBytes(UTF8).length vs just .getLength() because the AWS documentation limits type by bytes. int typeLengthBytes = type.getBytes(UTF8).length; if (typeLengthBytes > SimpleQueueProperties.MAX_MESSAGE_ATTRIBUTE_TYPE_LENGTH) { throw new InvalidParameterValueException("Message attribute type can not be longer than " + SimpleQueueProperties.MAX_MESSAGE_ATTRIBUTE_TYPE_LENGTH + " bytes"); } attributeValueLength += typeLengthBytes; if (value.getBinaryListValue() != null && !value.getBinaryListValue().isEmpty()) { throw new UnsupportedOperationException("Message attribute list values are not supported."); } if (value.getStringListValue() != null && !value.getStringListValue().isEmpty()) { throw new UnsupportedOperationException("Message attribute list values are not supported."); } int numberOfNonNullAndNonEmptyFields = 0; byte[] binaryValueByteArray = null; if (value.getBinaryValue() != null) { try { binaryValueByteArray = Base64.decode(value.getBinaryValue()); } catch (Base64DecodingException e) { throw new InvalidParameterValueException("The message attribute '" + name + "' contains an invalid Base64 Encoded String as a binary value"); } if ((binaryValueByteArray != null || binaryValueByteArray.length > 0)) { numberOfNonNullAndNonEmptyFields++; } } if (!Strings.isNullOrEmpty(value.getStringValue())) { numberOfNonNullAndNonEmptyFields++; } // we should also probably check the string list and binary list fields, but they are currently unsupported anyway if (numberOfNonNullAndNonEmptyFields == 0) { throw new InvalidParameterValueException("The message attribute '" + name + "' must contain non-empty message attribute value for message attribute type '" + type + "'."); } if (numberOfNonNullAndNonEmptyFields > 1) { throw new InvalidParameterValueException("Message attribute '" + name + "' has multiple values."); } if (isNumberType || isStringType) { if (Strings.isNullOrEmpty(value.getStringValue())) { throw new InvalidParameterValueException("The message attribute '" + name + "' with type '" + (isNumberType ? "Number" : "String") + "' must use field 'String'."); } // verify ok characters for (int codePoint : value.getStringValue().codePoints().toArray()) { if (!validMessageBodyCodePoints.contains(codePoint)) { throw new InvalidParameterValueException("Invalid binary character '#x" + Integer.toHexString(codePoint) + "' was found in the message attribute '" + name + "' value, the set of allowed characters is #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]"); } } // verify number if number if (isNumberType) { try { Double.parseDouble(value.getStringValue()); } catch (NumberFormatException e) { throw new InvalidParameterValueException("Could not cast message attribute '" + name + "' value to number."); } } // we have a (successful) string or number, add to length attributeValueLength += value.getStringValue().getBytes(UTF8).length; } else { // binary if (binaryValueByteArray == null || binaryValueByteArray.length == 0) { throw new InvalidParameterValueException("The message attribute '" + name + "' with type 'Binary' must use field 'Binary'."); } // we have a (successful) binary, add to length attributeValueLength += binaryValueByteArray.length; } return attributeValueLength; } private static class MessageInfo { private Message message; private int messageLength; Map<String, String> sendAttributes = Maps.newHashMap(); public Message getMessage() { return message; } public void setMessage(Message message) { this.message = message; } public int getMessageLength() { return messageLength; } public void setMessageLength(int messageLength) { this.messageLength = messageLength; } public Map<String, String> getSendAttributes() { return sendAttributes; } public void setSendAttributes(Map<String, String> sendAttributes) { this.sendAttributes = sendAttributes; } private MessageInfo(Message message, int messageLength, Map<String, String> sendAttributes) { this.message = message; this.messageLength = messageLength; this.sendAttributes = sendAttributes; } } private static MessageInfo validateAndGetMessageInfo(Queue queue, String senderId, String body, Integer delaySeconds, ArrayList<MessageAttribute> messageAttributes) throws EucalyptusCloudException { int messageLength = 0; Map<String, String> sendAttributes = Maps.newHashMap(); Message message = new Message(); if (delaySeconds != null) { if (delaySeconds < 0 || delaySeconds > SimpleQueueProperties.MAX_DELAY_SECONDS) { throw new InvalidParameterValueException("DelaySeconds must be a number between 0 and " + SimpleQueueProperties.MAX_DELAY_SECONDS); } sendAttributes.put(Constants.DELAY_SECONDS, "" + delaySeconds); } // check message attributes if (messageAttributes != null) { Set<String> usedAttributeNames = Sets.newHashSet(); for (MessageAttribute messageAttribute : messageAttributes) { if (messageAttribute == null) { throw new InvalidParameterValueException("Message attribute can not be null"); } messageLength += validateMessageAttributeNameAndCalculateLength(messageAttribute.getName(), usedAttributeNames); messageLength += validateMessageAttributeValueAndCalculateLength(messageAttribute.getValue(), messageAttribute.getName()); } message.setmD5OfMessageAttributes(calculateMessageAttributesMd5(convertMessageAttributesToMap(messageAttributes))); } if (Strings.isNullOrEmpty(body)) { throw new MissingParameterException("The request must contain the parameter MessageBody."); } // verify ok characters for (int codePoint : body.codePoints().toArray()) { if (!validMessageBodyCodePoints.contains(codePoint)) { throw new InvalidParameterValueException("Invalid binary character '#x" + Integer.toHexString(codePoint) + "' was " + "found in the message body, the set of allowed characters is #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]"); } } messageLength += body.getBytes(UTF8).length; if (messageLength > queue.getMaximumMessageSize()) { throw new InvalidParameterValueException("The message exceeds the maximum message length of the queue, which is " + queue.getMaximumMessageSize() + " bytes"); } message.setmD5OfBody(calculateMessageBodyMd5(body)); message.setBody(body); if (messageAttributes != null) { message.getMessageAttribute().addAll(messageAttributes); } message.getAttribute().add(new Attribute(Constants.SENDER_ID, senderId)); String messageId = PersistenceFactory.getMessagePersistence().getNewMessageUUID().toString(); message.setMessageId(messageId); return new MessageInfo(message, messageLength, sendAttributes); } private String getSenderId(Context ctx) { if (Principals.isSameUser(ctx.getUser(), Principals.nobodyUser())) { // use ip address on anonymous user return ctx.getRemoteAddress().getHostAddress(); } else { return ctx.getUser().getAuthenticatedId(); } } public SendMessageResponseType sendMessage(SendMessageType request) throws EucalyptusCloudException { SendMessageResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); MessageInfo messageInfo = validateAndGetMessageInfo(queue, getSenderId(ctx), request.getMessageBody(), request.getDelaySeconds(), request.getMessageAttribute()); PersistenceFactory.getMessagePersistence().sendMessage(queue, messageInfo.getMessage(), messageInfo.getSendAttributes()); reply.getSendMessageResult().setmD5OfMessageAttributes(messageInfo.getMessage().getmD5OfMessageAttributes()); reply.getSendMessageResult().setMessageId(messageInfo.getMessage().getMessageId()); reply.getSendMessageResult().setmD5OfMessageBody(messageInfo.getMessage().getmD5OfBody()); int delaySeconds = request.getDelaySeconds() == null ? queue.getDelaySeconds() : request.getDelaySeconds().intValue(); if (SimpleQueueProperties.ENABLE_LONG_POLLING) { sendMessageNotificationsScheduledExecutorService.schedule(() -> NotifyClient.notifyQueue(queue), delaySeconds, TimeUnit.SECONDS); } if (SimpleQueueProperties.ENABLE_METRICS_COLLECTION) { Date now = new Date(); PutMetricDataType putMetricDataType = CloudWatchClient.getSQSPutMetricDataType(queue.getKey()); CloudWatchClient.addSQSMetricDatum(putMetricDataType, queue.getKey(), now, Constants.NUMBER_OF_MESSAGES_SENT, 1.0, "Count"); CloudWatchClient.addSQSMetricDatum(putMetricDataType, queue.getKey(), now, Constants.SENT_MESSAGE_SIZE, (double) messageInfo.getMessageLength(), "Bytes"); CloudWatchClient.putMetricData(putMetricDataType); } } catch (Exception ex) { handleException(ex); } return reply; } public SetQueueAttributesResponseType setQueueAttributes(SetQueueAttributesType request) throws EucalyptusCloudException { SetQueueAttributesResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); Map<String, String> existingAttributes = queue.getAttributes(); setAndValidateAttributes(queue.getAccountId(), request.getAttribute(), existingAttributes); existingAttributes.put(Constants.LAST_MODIFIED_TIMESTAMP, String.valueOf(currentTimeSeconds())); PersistenceFactory.getQueuePersistence().updateQueueAttributes(queue.getAccountId(), queue.getQueueName(), existingAttributes); } catch (Exception ex) { handleException(ex); } return reply; } public ChangeMessageVisibilityBatchResponseType changeMessageVisibilityBatch(ChangeMessageVisibilityBatchType request) throws EucalyptusCloudException { ChangeMessageVisibilityBatchResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); if (request.getChangeMessageVisibilityBatchRequestEntry() == null || request.getChangeMessageVisibilityBatchRequestEntry().isEmpty()) { throw new EmptyBatchRequestException("There should be at least one ChangeMessageVisibilityBatchRequestEntry in the request."); } if (request.getChangeMessageVisibilityBatchRequestEntry().size() > SimpleQueueProperties.MAX_NUM_BATCH_ENTRIES) { throw new TooManyEntriesInBatchRequestException("Maximum number of entries per request are " + SimpleQueueProperties.MAX_NUM_BATCH_ENTRIES + ". You have sent " + request.getChangeMessageVisibilityBatchRequestEntry().size() + "."); } Set<String> previousIds = Sets.newHashSet(); Pattern batchIdPattern = Pattern.compile("[A-Za-z0-9_-]+"); for (ChangeMessageVisibilityBatchRequestEntry batchRequestEntry: request.getChangeMessageVisibilityBatchRequestEntry()) { if (batchRequestEntry.getId() == null || batchRequestEntry.getId().isEmpty()) { throw new MissingParameterException("A batch entry id is a required field"); } if (batchRequestEntry.getId().length() > SimpleQueueProperties.MAX_BATCH_ID_LENGTH || !batchIdPattern.matcher(batchRequestEntry.getId()).matches()) { throw new InvalidBatchEntryIdException("A batch entry id can only contain alphanumeric characters, hyphens and underscores. It can be at most "+ SimpleQueueProperties.MAX_BATCH_ID_LENGTH+" letters long."); } if (previousIds.contains(batchRequestEntry.getId())) { throw new BatchEntryIdsNotDistinctException("A batch entry id is duplicated in this request"); } previousIds.add(batchRequestEntry.getId()); } for (ChangeMessageVisibilityBatchRequestEntry batchRequestEntry: request.getChangeMessageVisibilityBatchRequestEntry()) { try { handleChangeMessageVisibility(batchRequestEntry.getVisibilityTimeout(), batchRequestEntry.getReceiptHandle(), queue); ChangeMessageVisibilityBatchResultEntry success = new ChangeMessageVisibilityBatchResultEntry(); success.setId(batchRequestEntry.getId()); reply.getChangeMessageVisibilityBatchResult().getChangeMessageVisibilityBatchResultEntry().add(success); } catch (Exception ex) { try { handleException(ex); } catch (SimpleQueueException ex1) { BatchResultErrorEntry failure = new BatchResultErrorEntry(); failure.setId(batchRequestEntry.getId()); failure.setCode(ex1.getCode()); failure.setMessage(ex1.getMessage()); failure.setSenderFault(ex1.getRole() != null && ex1.getRole().equals(Role.Sender)); reply.getChangeMessageVisibilityBatchResult().getBatchResultErrorEntry().add(failure); } } } } catch (Exception ex) { handleException(ex); } return reply; } public DeleteMessageBatchResponseType deleteMessageBatch(DeleteMessageBatchType request) throws EucalyptusCloudException { DeleteMessageBatchResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); if (request.getDeleteMessageBatchRequestEntry() == null || request.getDeleteMessageBatchRequestEntry().isEmpty()) { throw new EmptyBatchRequestException("There should be at least one DeleteMessageBatchRequestEntry in the request."); } if (request.getDeleteMessageBatchRequestEntry().size() > SimpleQueueProperties.MAX_NUM_BATCH_ENTRIES) { throw new TooManyEntriesInBatchRequestException("Maximum number of entries per request are " + SimpleQueueProperties.MAX_NUM_BATCH_ENTRIES + ". You have sent " + request.getDeleteMessageBatchRequestEntry().size() + "."); } Set<String> previousIds = Sets.newHashSet(); Pattern batchIdPattern = Pattern.compile("[A-Za-z0-9_-]+"); for (DeleteMessageBatchRequestEntry batchRequestEntry: request.getDeleteMessageBatchRequestEntry()) { if (batchRequestEntry.getId() == null || batchRequestEntry.getId().isEmpty()) { throw new MissingParameterException("A batch entry id is a required field"); } if (batchRequestEntry.getId().length() > SimpleQueueProperties.MAX_BATCH_ID_LENGTH || !batchIdPattern.matcher(batchRequestEntry.getId()).matches()) { throw new InvalidBatchEntryIdException("A batch entry id can only contain alphanumeric characters, hyphens and underscores. It can be at most "+ SimpleQueueProperties.MAX_BATCH_ID_LENGTH+" letters long."); } if (previousIds.contains(batchRequestEntry.getId())) { throw new BatchEntryIdsNotDistinctException("A batch entry id is duplicated in this request"); } previousIds.add(batchRequestEntry.getId()); } Date now = new Date(); int numSuccessfulRealDeletes = 0; for (DeleteMessageBatchRequestEntry batchRequestEntry: request.getDeleteMessageBatchRequestEntry()) { try { if (PersistenceFactory.getMessagePersistence().deleteMessage(queue.getKey(), batchRequestEntry.getReceiptHandle())) { // note: only send a CW metric if we actually delete a message. We can still 'succeed' on a stale // receipt handle. numSuccessfulRealDeletes++; } DeleteMessageBatchResultEntry success = new DeleteMessageBatchResultEntry(); success.setId(batchRequestEntry.getId()); reply.getDeleteMessageBatchResult().getDeleteMessageBatchResultEntry().add(success); } catch (Exception ex) { try { handleException(ex); } catch (SimpleQueueException ex1) { BatchResultErrorEntry failure = new BatchResultErrorEntry(); failure.setId(batchRequestEntry.getId()); failure.setCode(ex1.getCode()); failure.setMessage(ex1.getMessage()); failure.setSenderFault(ex1.getRole() != null && ex1.getRole().equals(Role.Sender)); reply.getDeleteMessageBatchResult().getBatchResultErrorEntry().add(failure); } } } if (SimpleQueueProperties.ENABLE_METRICS_COLLECTION && numSuccessfulRealDeletes > 0) { PutMetricDataType putMetricDataType = CloudWatchClient.getSQSPutMetricDataType(queue.getKey()); CloudWatchClient.addSQSMetricDatum(putMetricDataType, queue.getKey(), new Date(), Constants.NUMBER_OF_MESSAGES_DELETED, numSuccessfulRealDeletes, 1.0, 1.0, numSuccessfulRealDeletes, "Count"); CloudWatchClient.putMetricData(putMetricDataType); } } catch (Exception ex) { handleException(ex); } return reply; } public SendMessageBatchResponseType sendMessageBatch(SendMessageBatchType request) throws EucalyptusCloudException { SendMessageBatchResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); final Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); if (request.getSendMessageBatchRequestEntry() == null || request.getSendMessageBatchRequestEntry().isEmpty()) { throw new EmptyBatchRequestException("There should be at least one SendMessageBatchRequestEntry in the request."); } if (request.getSendMessageBatchRequestEntry().size() > SimpleQueueProperties.MAX_NUM_BATCH_ENTRIES) { throw new TooManyEntriesInBatchRequestException("Maximum number of entries per request are " + SimpleQueueProperties.MAX_NUM_BATCH_ENTRIES + ". You have sent " + request.getSendMessageBatchRequestEntry().size() + "."); } Set<String> previousIds = Sets.newHashSet(); Pattern batchIdPattern = Pattern.compile("[A-Za-z0-9_-]+"); for (SendMessageBatchRequestEntry batchRequestEntry: request.getSendMessageBatchRequestEntry()) { if (batchRequestEntry.getId() == null || batchRequestEntry.getId().isEmpty()) { throw new MissingParameterException("A batch entry id is a required field"); } if (batchRequestEntry.getId().length() > SimpleQueueProperties.MAX_BATCH_ID_LENGTH || !batchIdPattern.matcher(batchRequestEntry.getId()).matches()) { throw new InvalidBatchEntryIdException("A batch entry id can only contain alphanumeric characters, hyphens and underscores. It can be at most "+ SimpleQueueProperties.MAX_BATCH_ID_LENGTH+" letters long."); } if (previousIds.contains(batchRequestEntry.getId())) { throw new BatchEntryIdsNotDistinctException("A batch entry id is duplicated in this request"); } previousIds.add(batchRequestEntry.getId()); } Map<String, MessageInfo> messageInfoMap = Maps.newLinkedHashMap(); int totalMessageLength = 0; for (SendMessageBatchRequestEntry batchRequestEntry: request.getSendMessageBatchRequestEntry()) { MessageInfo messageInfo = validateAndGetMessageInfo(queue, getSenderId(ctx), batchRequestEntry.getMessageBody(), batchRequestEntry.getDelaySeconds(), batchRequestEntry.getMessageAttribute()); totalMessageLength += messageInfo.getMessageLength(); if (totalMessageLength > queue.getMaximumMessageSize()) { throw new InvalidParameterValueException("The combined message lengths exceed the maximum message length of the queue, which is " + queue.getMaximumMessageSize() + " bytes"); } messageInfoMap.put(batchRequestEntry.getId(), messageInfo); } Date now = new Date(); int numSuccessfulMessages = 0; int totalSuccessfulMessagesLength = 0; Integer smallestSuccessfulMessageLength = null; Integer largestSuccessfulMessageLength = null; for (SendMessageBatchRequestEntry batchRequestEntry: request.getSendMessageBatchRequestEntry()) { try { MessageInfo messageInfo = messageInfoMap.get(batchRequestEntry.getId()); PersistenceFactory.getMessagePersistence().sendMessage(queue, messageInfo.getMessage(), messageInfo.getSendAttributes()); SendMessageBatchResultEntry success = new SendMessageBatchResultEntry(); success.setmD5OfMessageAttributes(messageInfo.getMessage().getmD5OfMessageAttributes()); success.setMessageId(messageInfo.getMessage().getMessageId()); success.setmD5OfMessageBody(messageInfo.getMessage().getmD5OfBody()); success.setId(batchRequestEntry.getId()); reply.getSendMessageBatchResult().getSendMessageBatchResultEntry().add(success); int delaySeconds = batchRequestEntry.getDelaySeconds() == null ? queue.getDelaySeconds() : batchRequestEntry.getDelaySeconds().intValue(); if (SimpleQueueProperties.ENABLE_LONG_POLLING) { sendMessageNotificationsScheduledExecutorService.schedule(() -> NotifyClient.notifyQueue(queue), delaySeconds, TimeUnit.SECONDS); } numSuccessfulMessages += 1; if (smallestSuccessfulMessageLength == null || smallestSuccessfulMessageLength < messageInfo.getMessageLength()) { smallestSuccessfulMessageLength = messageInfo.getMessageLength(); } if (largestSuccessfulMessageLength == null || largestSuccessfulMessageLength > messageInfo.getMessageLength()) { largestSuccessfulMessageLength = messageInfo.getMessageLength(); } totalSuccessfulMessagesLength += messageInfo.getMessageLength(); } catch (Exception ex) { try { handleException(ex); } catch (SimpleQueueException ex1) { BatchResultErrorEntry failure = new BatchResultErrorEntry(); failure.setId(batchRequestEntry.getId()); failure.setCode(ex1.getCode()); failure.setMessage(ex1.getMessage()); failure.setSenderFault(ex1.getRole() != null && ex1.getRole().equals(Role.Sender)); reply.getSendMessageBatchResult().getBatchResultErrorEntry().add(failure); } } } if (SimpleQueueProperties.ENABLE_METRICS_COLLECTION && numSuccessfulMessages > 0) { PutMetricDataType putMetricDataType = CloudWatchClient.getSQSPutMetricDataType(queue.getKey()); CloudWatchClient.addSQSMetricDatum(putMetricDataType, queue.getKey(), now, Constants.NUMBER_OF_MESSAGES_SENT, numSuccessfulMessages, 1.0, 1.0, numSuccessfulMessages, "Count"); CloudWatchClient.addSQSMetricDatum(putMetricDataType, queue.getKey(), now, Constants.SENT_MESSAGE_SIZE, numSuccessfulMessages, smallestSuccessfulMessageLength, largestSuccessfulMessageLength, totalSuccessfulMessagesLength, "Bytes"); CloudWatchClient.putMetricData(putMetricDataType); } } catch (Exception ex) { handleException(ex); } return reply; } public ListDeadLetterSourceQueuesResponseType listDeadLetterSourceQueues(ListDeadLetterSourceQueuesType request) throws EucalyptusCloudException { ListDeadLetterSourceQueuesResponseType reply = request.getReply(); try { final Context ctx = Contexts.lookup(); Queue queue = getAndCheckPermissionOnQueue(request.getQueueUrl()); String queueArn = queue.getArn(); Collection<Queue.Key> sourceQueues = PersistenceFactory.getQueuePersistence().listDeadLetterSourceQueues(queue.getAccountId(), queueArn); if (sourceQueues != null) { for (Queue.Key sourceQueue: sourceQueues) { reply.getListDeadLetterSourceQueuesResult().getQueueUrl().add(getQueueUrlFromQueueUrlParts(new QueueUrlParts(sourceQueue.getAccountId(), sourceQueue.getQueueName()))); } } } catch (Exception ex) { handleException(ex); } return reply; } private static Map<String, MessageAttributeValue> convertMessageAttributesToMap(Collection<MessageAttribute> messageAttributes) { // yay lambdas? return messageAttributes == null ? null : messageAttributes.stream().collect(Collectors.toMap(MessageAttribute::getName, MessageAttribute::getValue)); } private static void handleException(final Exception e) throws SimpleQueueException { final SimpleQueueException cause = Exceptions.findCause(e, SimpleQueueException.class); if (cause != null) { throw cause; } LOG.error( e, e ); final InternalFailureException exception = new InternalFailureException(String.valueOf(e.getMessage())); if (Contexts.lookup().hasAdministrativePrivileges()) { exception.initCause(e); } throw exception; } // #x9 | #xA | #xD | [#x20 to #xD7FF] | [#xE000 to #xFFFD] | [#x10000 to #x10FFFF] private static RangeSet<Integer> validMessageBodyCodePoints = ImmutableRangeSet.<Integer>builder() .add(Range.singleton(0x9)) .add(Range.singleton(0xA)) .add(Range.singleton(0XD)) .add(Range.closed(0x20, 0xD7FF)) .add(Range.closed(0xE000, 0xFFFD)) .add(Range.closed(0x10000, 0x10FFFF)) .build(); // dash, dot, alphanumeric, underscore private static RangeSet<Integer> validMessageNameCodePoints = ImmutableRangeSet.<Integer>builder() .add(Range.singleton((int) '-')) .add(Range.singleton((int) '.')) .add(Range.closed((int) '0', (int) '9')) .add(Range.closed((int) 'A', (int) 'Z')) .add(Range.singleton((int) '_')) .add(Range.closed((int) 'a', (int) 'z')) .build(); private static void handleQueuePollingForReceive( final Queue queue, final ReceiveMessageResponseType response, final Callable<? extends ReceiveMessageResult> resultCallable, final long pollTimeout) throws AuthException { try { NotifyClient.pollQueue(queue, pollTimeout, Contexts.consumerWithCurrentContext( (notified) -> { try { if (notified) { final ReceiveMessageResult receiveMessageResult = resultCallable.call(); if (receiveMessageResult != null) { response.setReceiveMessageResult(receiveMessageResult); Contexts.response(response); return; } else if (System.currentTimeMillis() < pollTimeout) { handleQueuePollingForReceive(queue, response, resultCallable, pollTimeout); return; } } sendEmptyReceiveCW(queue); Contexts.response(response); } catch (final InterruptedException e) { LOG.info("Interrupted while polling for task " + queue.getArn(), e); } catch (final Exception e) { LOG.error("Error polling for task " + queue.getArn(), e); } } )); } catch ( Exception e ) { LOG.error("Error polling for task " + queue.getArn(), e); sendEmptyReceiveCW(queue); Contexts.response( response ); } } public static long currentTimeSeconds() { return System.currentTimeMillis() / 1000L; } private static class QueueResolver implements Function<String,Queue> { private final Queue queue; private QueueResolver( final Queue queue ) { this.queue = queue; } @Override public Queue apply( @Nullable final String queueFullName ) { return queue; } } // BEGIN CODE FROM Amazon AWS SDK 1.11.28-SNAPSHOT, file: com.amazonaws.services.sqs.MessageMD5ChecksumHandler /** * Returns the hex-encoded MD5 hash String of the given message body. */ private static final int INTEGER_SIZE_IN_BYTES = 4; private static final byte STRING_TYPE_FIELD_INDEX = 1; private static final byte BINARY_TYPE_FIELD_INDEX = 2; private static final byte STRING_LIST_TYPE_FIELD_INDEX = 3; private static final byte BINARY_LIST_TYPE_FIELD_INDEX = 4; private static String calculateMessageBodyMd5(String messageBody) throws EucalyptusCloudException { if (LOG.isTraceEnabled()) { LOG.trace("Message body: " + messageBody); } byte[] expectedMd5; try { expectedMd5 = Md5Utils.computeMD5Hash(messageBody.getBytes(UTF8)); } catch (Exception e) { throw new EucalyptusCloudException("Unable to calculate the MD5 hash of the message body. " + e.getMessage(), e); } String expectedMd5Hex = BinaryUtils.toHex(expectedMd5); if (LOG.isTraceEnabled()) { LOG.trace("Expected MD5 of message body: " + expectedMd5Hex); } return expectedMd5Hex; } /** * Returns the hex-encoded MD5 hash String of the given message attributes. */ private static String calculateMessageAttributesMd5(final Map<String, MessageAttributeValue> messageAttributes) throws EucalyptusCloudException { if (LOG.isTraceEnabled()) { LOG.trace("Message attribtues: " + messageAttributes); } List<String> sortedAttributeNames = new ArrayList<String>(messageAttributes.keySet()); Collections.sort(sortedAttributeNames); MessageDigest md5Digest = null; try { md5Digest = MessageDigest.getInstance("MD5"); for (String attrName : sortedAttributeNames) { MessageAttributeValue attrValue = messageAttributes.get(attrName); // Encoded Name updateLengthAndBytes(md5Digest, attrName); // Encoded Type updateLengthAndBytes(md5Digest, attrValue.getDataType()); // Encoded Value if (attrValue.getStringValue() != null) { md5Digest.update(STRING_TYPE_FIELD_INDEX); updateLengthAndBytes(md5Digest, attrValue.getStringValue()); } else if (attrValue.getBinaryValue() != null) { md5Digest.update(BINARY_TYPE_FIELD_INDEX); // Eucalyptus stores the value as a Base 64 encoded string. Convert to byte buffer ByteBuffer byteBuffer = ByteBuffer.wrap(Base64.decode(attrValue.getBinaryValue())); updateLengthAndBytes(md5Digest, byteBuffer); } else if (attrValue.getStringListValue() != null && attrValue.getStringListValue().size() > 0) { md5Digest.update(STRING_LIST_TYPE_FIELD_INDEX); for (String strListMember : attrValue.getStringListValue()) { updateLengthAndBytes(md5Digest, strListMember); } } else if (attrValue.getBinaryListValue() != null && attrValue.getBinaryListValue().size() > 0) { md5Digest.update(BINARY_LIST_TYPE_FIELD_INDEX); for (String byteListMember : attrValue.getBinaryListValue()) { // Eucalyptus stores the value as a Base 64 encoded string. Convert to byte buffer ByteBuffer byteBuffer = ByteBuffer.wrap(Base64.decode(byteListMember)); updateLengthAndBytes(md5Digest, byteBuffer); } } } } catch (Exception e) { throw new EucalyptusCloudException("Unable to calculate the MD5 hash of the message attributes. " + e.getMessage(), e); } String expectedMd5Hex = BinaryUtils.toHex(md5Digest.digest()); if (LOG.isTraceEnabled()) { LOG.trace("Expected MD5 of message attributes: " + expectedMd5Hex); } return expectedMd5Hex; } /** * Update the digest using a sequence of bytes that consists of the length (in 4 bytes) of the * input String and the actual utf8-encoded byte values. */ private static void updateLengthAndBytes(MessageDigest digest, String str) throws UnsupportedEncodingException { byte[] utf8Encoded = str.getBytes(UTF8); ByteBuffer lengthBytes = ByteBuffer.allocate(INTEGER_SIZE_IN_BYTES).putInt(utf8Encoded.length); digest.update(lengthBytes.array()); digest.update(utf8Encoded); } /** * Update the digest using a sequence of bytes that consists of the length (in 4 bytes) of the * input ByteBuffer and all the bytes it contains. */ private static void updateLengthAndBytes(MessageDigest digest, ByteBuffer binaryValue) { ByteBuffer readOnlyBuffer = binaryValue.asReadOnlyBuffer(); int size = readOnlyBuffer.remaining(); ByteBuffer lengthBytes = ByteBuffer.allocate(INTEGER_SIZE_IN_BYTES).putInt(size); digest.update(lengthBytes.array()); digest.update(readOnlyBuffer); } // From com.amazonaws.util.StringUtils: public static final Charset UTF8 = StandardCharsets.UTF_8; // END CODE FROM Amazon AWS SDK 1.11.28-SNAPSHOT }