serverless-offline-sns
Version:
Serverless plugin to run a local SNS server and call lambdas with events notifications.
332 lines (331 loc) • 13.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var aws_sdk_1 = require("aws-sdk");
var node_fetch_1 = require("node-fetch");
var url_1 = require("url");
var bodyParser = require("body-parser");
var _ = require("lodash");
var xml = require("xml");
var helpers_1 = require("./helpers");
var SNSServer = /** @class */ (function () {
function SNSServer(debug, app, region, accountId) {
this.pluginDebug = debug;
this.topics = [];
this.subscriptions = [];
this.app = app;
this.region = region;
this.routes();
this.accountId = accountId;
}
SNSServer.prototype.routes = function () {
var _this = this;
this.debug("configuring route");
this.app.use(bodyParser.json()); // for parsing application/json
this.app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
this.app.use(function (req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
this.app.all("/", function (req, res) {
_this.debug("hello request");
_this.debug(JSON.stringify(req.body));
_this.debug(JSON.stringify(_this.subscriptions));
if (req.body.Action === "ListSubscriptions") {
_this.debug("sending: " + xml(_this.listSubscriptions(), { indent: "\t" }));
res.send(xml(_this.listSubscriptions()));
}
else if (req.body.Action === "ListTopics") {
_this.debug("sending: " + xml(_this.listTopics(), { indent: "\t" }));
res.send(xml(_this.listTopics()));
}
else if (req.body.Action === "CreateTopic") {
res.send(xml(_this.createTopic(req.body.Name)));
}
else if (req.body.Action === "Subscribe") {
res.send(xml(_this.subscribe(req.body.Endpoint, req.body.Protocol, req.body.TopicArn, req.body)));
}
else if (req.body.Action === "Publish") {
var target = _this.extractTarget(req.body);
if (req.body.MessageStructure === "json") {
var json = JSON.parse(req.body.Message);
if (typeof json.default !== "string") {
throw new Error("Messages must have default key");
}
}
res.send(xml(_this.publish(target, req.body.Subject, req.body.Message, req.body.MessageStructure, helpers_1.parseMessageAttributes(req.body))));
}
else if (req.body.Action === "Unsubscribe") {
res.send(xml(_this.unsubscribe(req.body.SubscriptionArn)));
}
else {
res.send(xml({
NotImplementedResponse: [
helpers_1.createAttr(),
helpers_1.createMetadata(),
],
}));
}
_this.debug(JSON.stringify(_this.subscriptions));
});
};
SNSServer.prototype.listTopics = function () {
this.debug("Topics: " + JSON.stringify(this.topics));
return {
ListTopicsResponse: [
helpers_1.createAttr(),
helpers_1.createMetadata(),
{
ListTopicsResult: [{
Topics: this.topics.map(function (topic) {
return {
member: helpers_1.arrayify({
TopicArn: topic.TopicArn,
}),
};
}),
}],
},
],
};
};
SNSServer.prototype.listSubscriptions = function () {
this.debug(this.subscriptions.map(function (sub) {
return {
member: [sub],
};
}));
return {
ListSubscriptionsResponse: [
helpers_1.createAttr(),
helpers_1.createMetadata(),
{
ListSubscriptionsResult: [{
Subscriptions: this.subscriptions.map(function (sub) {
return {
member: helpers_1.arrayify({
Endpoint: sub.Endpoint,
TopicArn: sub.TopicArn,
Owner: sub.Owner,
Protocol: sub.Protocol,
SubscriptionArn: sub.SubscriptionArn,
}),
};
}),
}],
},
],
};
};
SNSServer.prototype.unsubscribe = function (arn) {
this.debug(JSON.stringify(this.subscriptions));
this.debug("unsubscribing: " + arn);
this.subscriptions = this.subscriptions.filter(function (sub) { return sub.SubscriptionArn !== arn; });
return {
UnsubscribeResponse: [
helpers_1.createAttr(),
helpers_1.createMetadata(),
],
};
};
SNSServer.prototype.createTopic = function (topicName) {
var topicArn = helpers_1.topicArnFromName(topicName, this.region, this.accountId);
var topic = {
TopicArn: topicArn,
};
if (!this.topics.find(function (_a) {
var TopicArn = _a.TopicArn;
return TopicArn === topicArn;
})) {
this.topics.push(topic);
}
return {
CreateTopicResponse: [
helpers_1.createAttr(),
helpers_1.createMetadata(),
{
CreateTopicResult: [
{
TopicArn: topicArn,
},
],
},
],
};
};
SNSServer.prototype.subscribe = function (endpoint, protocol, arn, body) {
var attributes = helpers_1.parseAttributes(body);
var filterPolicies = attributes["FilterPolicy"] && JSON.parse(attributes["FilterPolicy"]);
arn = this.convertPseudoParams(arn);
var existingSubscription = this.subscriptions.find(function (subscription) {
return subscription.Endpoint === endpoint && subscription.TopicArn === arn;
});
var subscriptionArn;
if (!existingSubscription) {
var sub = {
SubscriptionArn: arn + ":" + Math.floor(Math.random() * (1000000 - 1)),
Protocol: protocol,
TopicArn: arn,
Endpoint: endpoint,
Owner: "",
Attributes: attributes,
Policies: filterPolicies,
};
this.subscriptions.push(sub);
subscriptionArn = sub.SubscriptionArn;
}
else {
subscriptionArn = existingSubscription.SubscriptionArn;
}
return {
SubscribeResponse: [
helpers_1.createAttr(),
helpers_1.createMetadata(),
{
SubscribeResult: [
{
SubscriptionArn: subscriptionArn,
},
],
},
],
};
};
SNSServer.prototype.evaluatePolicies = function (policies, messageAttrs) {
var shouldSend = false;
for (var _i = 0, _a = Object.entries(policies); _i < _a.length; _i++) {
var _b = _a[_i], k = _b[0], v = _b[1];
if (!messageAttrs[k]) {
shouldSend = false;
break;
}
var attrs = void 0;
if (messageAttrs[k].Type.endsWith(".Array")) {
attrs = JSON.parse(messageAttrs[k].Value);
}
else {
attrs = [messageAttrs[k].Value];
}
if (_.intersection(v, attrs).length > 0) {
this.debug("filterPolicy Passed: " + v + " matched message attrs: " + JSON.stringify(attrs));
shouldSend = true;
}
else {
shouldSend = false;
break;
}
}
if (!shouldSend) {
this.debug("filterPolicy Failed: " + JSON.stringify(policies) + " did not match message attrs: " + JSON.stringify(messageAttrs));
}
return shouldSend;
};
SNSServer.prototype.publishHttp = function (event, sub, raw) {
var _this = this;
return node_fetch_1.default(sub.Endpoint, {
method: "POST",
body: event,
timeout: 0,
headers: {
"x-amz-sns-rawdelivery": "" + raw,
"Content-Type": "text/plain; charset=UTF-8",
"Content-Length": Buffer.byteLength(event),
},
}).then(function (res) { return _this.debug(res); })
.catch(function (ex) { return _this.debug(ex); });
};
SNSServer.prototype.publishSqs = function (event, sub) {
var subEndpointUrl = new url_1.URL(sub.Endpoint);
var sqsEndpoint = subEndpointUrl.protocol + "//" + subEndpointUrl.host + "/";
var sqs = new aws_sdk_1.SQS({ endpoint: sqsEndpoint, region: this.region });
if (sub["Attributes"]["RawMessageDelivery"] === "true") {
return sqs.sendMessage({
QueueUrl: sub.Endpoint,
MessageBody: event,
}).promise();
}
else {
var records = JSON.parse(event).Records;
var messagePromises = records.map(function (record) {
return sqs
.sendMessage({
QueueUrl: sub.Endpoint,
MessageBody: JSON.stringify(record.Sns),
})
.promise();
});
return Promise.all(messagePromises);
}
};
SNSServer.prototype.publish = function (topicArn, subject, message, messageStructure, messageAttributes) {
var _this = this;
var messageId = helpers_1.createMessageId();
Promise.all(this.subscriptions.filter(function (sub) { return sub.TopicArn === topicArn; }).map(function (sub) {
var isRaw = sub["Attributes"]["RawMessageDelivery"] === "true";
if (sub["Policies"] && !_this.evaluatePolicies(sub["Policies"], messageAttributes)) {
_this.debug("Filter policies failed. Skipping subscription: " + sub.Endpoint);
return;
}
_this.debug("fetching: " + sub.Endpoint);
var event;
if (isRaw) {
event = message;
}
else {
event = JSON.stringify(helpers_1.createSnsTopicEvent(topicArn, sub.SubscriptionArn, subject, message, messageId, messageStructure, messageAttributes));
}
_this.debug("event: " + event);
if (!sub.Protocol) {
sub.Protocol = "http";
}
var protocol = sub.Protocol.toLowerCase();
if (protocol === "http") {
return _this.publishHttp(event, sub, isRaw);
}
if (protocol === "sqs") {
return _this.publishSqs(event, sub);
}
throw new Error("Protocol '" + protocol + "' is not supported by serverless-offline-sns");
}));
return {
PublishResponse: [
helpers_1.createAttr(),
{
PublishResult: [
{
MessageId: messageId,
},
],
},
helpers_1.createMetadata(),
],
};
};
SNSServer.prototype.extractTarget = function (body) {
if (!body.PhoneNumber) {
var target = body.TopicArn || body.TargetArn;
if (!target) {
throw new Error("TopicArn or TargetArn is missing");
}
return this.convertPseudoParams(target);
}
else {
return helpers_1.validatePhoneNumber(body.PhoneNumber);
}
};
SNSServer.prototype.convertPseudoParams = function (topicArn) {
var awsRegex = /#{AWS::([a-zA-Z]+)}/g;
return topicArn.replace(awsRegex, this.accountId);
};
SNSServer.prototype.debug = function (msg) {
if (msg instanceof Object) {
try {
msg = JSON.stringify(msg);
}
catch (ex) { }
}
this.pluginDebug(msg, "server");
};
return SNSServer;
}());
exports.SNSServer = SNSServer;