'use strict';
|
|
const assert = require('assert');
|
|
const debug = require('debug')('mns:client');
|
const httpx = require('httpx');
|
const kitx = require('kitx');
|
|
const {
|
getEndpoint,
|
toXMLBuffer,
|
parseXML,
|
extract,
|
getResponseHeaders,
|
getCanonicalizedMNSHeaders
|
} = require('./helper');
|
|
class Client {
|
constructor(accountid, opts) {
|
assert(accountid, '"accountid" must be passed in');
|
this.accountid = accountid;
|
assert(opts, 'must pass in "opts"');
|
// 兼容
|
const accessKeyID = opts.accessKeyId || opts.accessKeyID;
|
assert(accessKeyID, 'must pass in "opts.accessKeyID"');
|
this.accessKeyID = accessKeyID;
|
assert(opts.accessKeySecret, 'must pass in "opts.accessKeySecret"');
|
this.accessKeySecret = opts.accessKeySecret;
|
assert(opts.region, 'must pass in "opts.region"');
|
const {domain, endpoint} = getEndpoint(accountid, opts);
|
this.endpointDomain = domain;
|
this.endpoint = endpoint;
|
|
// security token
|
this.securityToken = opts.securityToken;
|
}
|
|
async request(method, resource, type, requestBody, attentions = [], opts = {}) {
|
const url = `${this.endpoint}${resource}`;
|
debug('url: %s', url);
|
debug('method: %s', method);
|
const headers = this.buildHeaders(method, requestBody, resource, opts.headers);
|
debug('request headers: %j', headers);
|
debug('request body: %s', requestBody.toString());
|
const response = await httpx.request(url, Object.assign(opts, {
|
method: method,
|
headers: headers,
|
data: requestBody
|
}));
|
|
debug('statusCode %s', response.statusCode);
|
debug('response headers: %j', response.headers);
|
const code = response.statusCode;
|
|
const contentType = response.headers['content-type'] || '';
|
// const contentLength = response.headers['content-length'];
|
const responseBody = await httpx.read(response, 'utf8');
|
debug('response body: %s', responseBody);
|
|
var body;
|
if (responseBody && (contentType.startsWith('text/xml') || contentType.startsWith('application/xml'))) {
|
const responseData = await parseXML(responseBody);
|
|
if (responseData.Error) {
|
const e = responseData.Error;
|
const message = extract(e.Message);
|
const requestid = extract(e.RequestId);
|
const hostid = extract(e.HostId);
|
const err = new Error(`${method} ${url} failed with ${code}. ` +
|
`requestid: ${requestid}, hostid: ${hostid}, message: ${message}`);
|
err.name = 'MNS' + extract(e.Code) + err.name;
|
throw err;
|
}
|
|
body = {};
|
Object.keys(responseData[type]).forEach((key) => {
|
if (key !== '$') {
|
body[key] = extract(responseData[type][key]);
|
}
|
});
|
}
|
|
return {
|
code,
|
headers: getResponseHeaders(response.headers, attentions),
|
body: body
|
};
|
}
|
|
get(resource, type, opts) {
|
return this.request('GET', resource, type, '', [], opts);
|
}
|
|
put(resource, type, body, attentions = [], opts = {}) {
|
return this.request('PUT', resource, type, body, attentions, opts);
|
}
|
|
post(resource, type, body) {
|
return this.request('POST', resource, type, body);
|
}
|
|
delete(resource, type, body) {
|
return this.request('DELETE', resource, type, body);
|
}
|
|
sign(verb, headers, resource) {
|
const canonicalizedMNSHeaders = getCanonicalizedMNSHeaders(headers);
|
const md5 = headers['content-md5'] || '';
|
const date = headers['date'];
|
const type = headers['content-type'] || '';
|
|
var toSignString = `${verb}\n${md5}\n${type}\n${date}\n${canonicalizedMNSHeaders}${resource}`;
|
// Signature = base64(hmac-sha1(VERB + "\n"
|
// + CONTENT-MD5 + "\n"
|
// + CONTENT-TYPE + "\n"
|
// + DATE + "\n"
|
// + CanonicalizedMNSHeaders
|
// + CanonicalizedResource))
|
var buff = Buffer.from(toSignString, 'utf8');
|
const degist = kitx.sha1(buff, this.accessKeySecret, 'binary');
|
return Buffer.from(degist, 'binary').toString('base64');
|
}
|
|
buildHeaders(method, body, resource, customHeaders) {
|
const date = new Date().toGMTString();
|
|
const headers = {
|
'date': date,
|
'host': this.endpointDomain,
|
'x-mns-date': date,
|
'x-mns-version': '2015-06-06'
|
};
|
|
if (method !== 'GET' && method !== 'HEAD') {
|
const contentType = 'text/xml';
|
const digest = kitx.md5(body, 'hex');
|
const md5 = Buffer.from(digest, 'utf8').toString('base64');
|
Object.assign(headers, {
|
'content-length': body.length,
|
'content-type': contentType,
|
'content-md5': md5
|
});
|
}
|
|
Object.assign(headers, customHeaders);
|
|
const signature = this.sign(method, headers, resource);
|
|
headers['authorization'] = `MNS ${this.accessKeyID}:${signature}`;
|
|
if (this.securityToken) {
|
headers['security-token'] = this.securityToken;
|
}
|
|
return headers;
|
}
|
|
// Queue
|
createQueue(name, params = {}) {
|
const body = toXMLBuffer('Queue', params);
|
const url = `/queues/${name}`;
|
return this.put(url, 'Queue', body, ['location']);
|
}
|
|
deleteQueue(name) {
|
return this.delete(`/queues/${name}`, 'Queue', '');
|
}
|
|
async listQueue(start, limit, prefix) {
|
var customHeaders = {};
|
if (typeof start !== 'undefined') {
|
customHeaders['x-mns-marker'] = start;
|
}
|
|
if (typeof limit !== 'undefined') {
|
customHeaders['x-mns-ret-number'] = limit;
|
}
|
|
if (typeof limit !== 'undefined') {
|
customHeaders['x-mns-prefix'] = prefix;
|
}
|
|
const subType = 'Queue';
|
const response = await this.get('/queues', 'Queues', {
|
headers: customHeaders
|
});
|
response.body = response.body[subType];
|
return response;
|
}
|
|
getQueueAttributes(queueName) {
|
return this.get(`/queues/${queueName}`, 'Queue');
|
}
|
|
setQueueAttributes(queueName, params = {}) {
|
const body = toXMLBuffer('Queue', params);
|
const url = `/queues/${queueName}?metaoverride=true`;
|
return this.put(url, 'Queue', body);
|
}
|
|
// Message
|
sendMessage(queueName, params) {
|
const url = `/queues/${queueName}/messages`;
|
const body = toXMLBuffer('Message', params);
|
return this.post(url, 'Message', body);
|
}
|
|
async batchSendMessage(queueName, params) {
|
const url = `/queues/${queueName}/messages`;
|
const subType = 'Message';
|
const body = toXMLBuffer('Messages', params, subType);
|
var response = await this.post(url, 'Messages', body);
|
response.body = response.body[subType];
|
return response;
|
}
|
|
receiveMessage(queueName, waitSeconds) {
|
var url = `/queues/${queueName}/messages`;
|
if (waitSeconds) {
|
url += `?waitseconds=${waitSeconds}`;
|
}
|
|
// 31000 31s +1s max waitSeconds is 30s
|
return this.get(url, 'Message', {timeout: 31000});
|
}
|
|
async batchReceiveMessage(queueName, numOfMessages, waitSeconds) {
|
var url = `/queues/${queueName}/messages?numOfMessages=${numOfMessages}`;
|
if (waitSeconds) {
|
url += `&waitseconds=${waitSeconds}`;
|
}
|
|
const subType = 'Message';
|
// 31000 31s +1s max waitSeconds is 30s
|
var response = await this.get(url, 'Messages', {timeout: 31000});
|
response.body = response.body[subType];
|
return response;
|
}
|
|
peekMessage(queueName) {
|
return this.get(`/queues/${queueName}/messages?peekonly=true`, 'Message');
|
}
|
|
async batchPeekMessage(queueName, numOfMessages) {
|
const url = `/queues/${queueName}/messages?` +
|
`peekonly=true&numOfMessages=${numOfMessages}`;
|
|
const subType = 'Message';
|
// 31000 31s +1s max waitSeconds is 30s
|
var response = await this.get(url, 'Messages');
|
response.body = response.body[subType];
|
return response;
|
}
|
|
deleteMessage(queueName, receiptHandle) {
|
const url = `/queues/${queueName}/messages?ReceiptHandle=${receiptHandle}`;
|
return this.delete(url, 'Message', '');
|
}
|
|
async batchDeleteMessage(queueName, receiptHandles) {
|
const body = toXMLBuffer('ReceiptHandles', receiptHandles, 'ReceiptHandle');
|
const url = `/queues/${queueName}/messages`;
|
const response = await this.delete(url, 'Errors', body);
|
// 3种情况,普通失败,部分失败,全部成功
|
if (response.body) {
|
const subType = 'Error';
|
// 部分失败
|
response.body = response.body[subType];
|
}
|
return response;
|
}
|
|
changeMessageVisibility(queueName, receiptHandle, visibilityTimeout) {
|
const url = `/queues/${queueName}/messages?` +
|
`receiptHandle=${receiptHandle}&visibilityTimeout=${visibilityTimeout}`;
|
return this.put(url, 'ChangeVisibility', '');
|
}
|
|
// Topic
|
createTopic(name, params = {}) {
|
const body = toXMLBuffer('Topic', params);
|
return this.put(`/topics/${name}`, 'Topic', body, ['location']);
|
}
|
|
deleteTopic() {}
|
async listTopic(start, limit, prefix) {
|
var customHeaders = {};
|
if (typeof start !== 'undefined') {
|
customHeaders['x-mns-marker'] = start;
|
}
|
|
if (typeof limit !== 'undefined') {
|
customHeaders['x-mns-ret-number'] = limit;
|
}
|
|
if (typeof limit !== 'undefined') {
|
customHeaders['x-mns-prefix'] = prefix;
|
}
|
|
const subType = 'Topic';
|
const response = await this.get('/topics', 'Topics', {
|
headers: customHeaders
|
});
|
response.body = response.body[subType];
|
return response;
|
}
|
|
getTopicAttributes(name) {
|
return this.get(`/topics/${name}`, 'Topic');
|
}
|
|
setTopicAttributes(name, params = {}) {
|
const body = toXMLBuffer('Topic', params);
|
const url = `/topics/${name}?metaoverride=true`;
|
return this.put(url, 'Topic', body);
|
}
|
|
// Subscription
|
subscribe() {}
|
unsubscribe() {}
|
listSubscriptionByTopic() {}
|
getSubscriptionAttributes() {}
|
setSubscriptionAttributes() {}
|
|
// Message
|
publishMessage(topic, params) {
|
const url = `/topics/${topic}/messages`;
|
const body = toXMLBuffer('Message', params);
|
return this.post(url, 'Message', body);
|
}
|
|
// Notifications
|
httpEndpoint() {}
|
}
|
|
module.exports = Client;
|