Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
plugins: [
"transform-decorators-legacy",
["transform-es2015-template-literals", { "loose": true }],
"transform-es2015-literals",
"transform-es2015-function-name",
"transform-es2015-arrow-functions",
"transform-es2015-block-scoped-functions",
["transform-es2015-classes", { "loose": true }],
"transform-es2015-object-super",
"transform-es2015-shorthand-properties",
["transform-es2015-computed-properties", { "loose": true }],
["transform-es2015-for-of", { "loose": true }],
"transform-es2015-sticky-regex",
"transform-es2015-unicode-regex",
"check-es2015-constants",
["transform-es2015-spread", { "loose": true }],
"transform-es2015-parameters",
["transform-es2015-destructuring", { "loose": true }],
"transform-es2015-block-scoping",
["transform-es2015-modules-commonjs", { "loose": true }],
"transform-object-rest-spread",
"transform-react-jsx",
"transform-async-to-generator",
"syntax-jsx"
]
}
234 changes: 169 additions & 65 deletions lib/wechat.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
'use strict';

var _xml2js = require('xml2js');

var _xml2js2 = _interopRequireDefault(_xml2js);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }

var getRawBody = require('raw-body');
var xml2js = require('xml2js');

var crypto = require('crypto');
var ejs = require('ejs');
var WXBizMsgCrypt = require('wechat-crypto');

var wechat = function (config) {
var wechat = function wechat(config) {
if (!(this instanceof wechat)) {
return new wechat(config);
}
Expand All @@ -23,24 +33,34 @@ wechat.prototype.setToken = function (config) {
}
};

var getSignature = function (timestamp, nonce, token) {
var getSignature = function getSignature(timestamp, nonce, token) {
var shasum = crypto.createHash('sha1');
var arr = [token, timestamp, nonce].sort();
shasum.update(arr.join(''));

return shasum.digest('hex');
};

var parseXML = function (xml) {
var parseXML = function parseXML(xml) {
return function (done) {
xml2js.parseString(xml, {trim: true}, done);
_xml2js2.default.parseString(xml, { trim: true }, done);
};
};

var parseXML2 = function parseXML2(xml) {
return new Promise(function (resolve, reject) {
_xml2js2.default.parseString(xml, { trim: true }, function (err, res) {
if (err) return reject(err);
if (arguments.length > 2) res = slice.call(arguments, 1);
resolve(res);
});
});
};

/*!
* 将xml2js解析出来的对象转换成直接可访问的对象
*/
var formatMessage = function (result) {
var formatMessage = function formatMessage(result) {
var message = {};
if (typeof result === 'object') {
for (var key in result) {
Expand Down Expand Up @@ -68,73 +88,21 @@ var formatMessage = function (result) {
/*!
* 响应模版
*/
var tpl = ['<xml>',
'<ToUserName><![CDATA[<%-toUsername%>]]></ToUserName>',
'<FromUserName><![CDATA[<%-fromUsername%>]]></FromUserName>',
'<CreateTime><%=createTime%></CreateTime>',
'<MsgType><![CDATA[<%=msgType%>]]></MsgType>',
'<% if (msgType === "news") { %>',
'<ArticleCount><%=content.length%></ArticleCount>',
'<Articles>',
'<% content.forEach(function(item){ %>',
'<item>',
'<Title><![CDATA[<%-item.title%>]]></Title>',
'<Description><![CDATA[<%-item.description%>]]></Description>',
'<PicUrl><![CDATA[<%-item.picUrl || item.picurl || item.pic %>]]></PicUrl>',
'<Url><![CDATA[<%-item.url%>]]></Url>',
'</item>',
'<% }); %>',
'</Articles>',
'<% } else if (msgType === "music") { %>',
'<Music>',
'<Title><![CDATA[<%-content.title%>]]></Title>',
'<Description><![CDATA[<%-content.description%>]]></Description>',
'<MusicUrl><![CDATA[<%-content.musicUrl || content.url %>]]></MusicUrl>',
'<HQMusicUrl><![CDATA[<%-content.hqMusicUrl || content.hqUrl %>]]></HQMusicUrl>',
'</Music>',
'<% } else if (msgType === "voice") { %>',
'<Voice>',
'<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>',
'</Voice>',
'<% } else if (msgType === "image") { %>',
'<Image>',
'<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>',
'</Image>',
'<% } else if (msgType === "video") { %>',
'<Video>',
'<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>',
'<Title><![CDATA[<%-content.title%>]]></Title>',
'<Description><![CDATA[<%-content.description%>]]></Description>',
'</Video>',
'<% } else if (msgType === "transfer_customer_service") { %>',
'<% if (content && content.kfAccount) { %>',
'<TransInfo>',
'<KfAccount><![CDATA[<%-content.kfAccount%>]]></KfAccount>',
'</TransInfo>',
'<% } %>',
'<% } else { %>',
'<Content><![CDATA[<%-content%>]]></Content>',
'<% } %>',
'</xml>'].join('');
var tpl = ['<xml>', '<ToUserName><![CDATA[<%-toUsername%>]]></ToUserName>', '<FromUserName><![CDATA[<%-fromUsername%>]]></FromUserName>', '<CreateTime><%=createTime%></CreateTime>', '<MsgType><![CDATA[<%=msgType%>]]></MsgType>', '<% if (msgType === "news") { %>', '<ArticleCount><%=content.length%></ArticleCount>', '<Articles>', '<% content.forEach(function(item){ %>', '<item>', '<Title><![CDATA[<%-item.title%>]]></Title>', '<Description><![CDATA[<%-item.description%>]]></Description>', '<PicUrl><![CDATA[<%-item.picUrl || item.picurl || item.pic %>]]></PicUrl>', '<Url><![CDATA[<%-item.url%>]]></Url>', '</item>', '<% }); %>', '</Articles>', '<% } else if (msgType === "music") { %>', '<Music>', '<Title><![CDATA[<%-content.title%>]]></Title>', '<Description><![CDATA[<%-content.description%>]]></Description>', '<MusicUrl><![CDATA[<%-content.musicUrl || content.url %>]]></MusicUrl>', '<HQMusicUrl><![CDATA[<%-content.hqMusicUrl || content.hqUrl %>]]></HQMusicUrl>', '</Music>', '<% } else if (msgType === "voice") { %>', '<Voice>', '<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>', '</Voice>', '<% } else if (msgType === "image") { %>', '<Image>', '<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>', '</Image>', '<% } else if (msgType === "video") { %>', '<Video>', '<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>', '<Title><![CDATA[<%-content.title%>]]></Title>', '<Description><![CDATA[<%-content.description%>]]></Description>', '</Video>', '<% } else if (msgType === "transfer_customer_service") { %>', '<% if (content && content.kfAccount) { %>', '<TransInfo>', '<KfAccount><![CDATA[<%-content.kfAccount%>]]></KfAccount>', '</TransInfo>', '<% } %>', '<% } else { %>', '<Content><![CDATA[<%-content%>]]></Content>', '<% } %>', '</xml>'].join('');

/*!
* 编译过后的模版
*/
var compiled = ejs.compile(tpl);

var wrapTpl = '<xml>' +
'<Encrypt><![CDATA[<%-encrypt%>]]></Encrypt>' +
'<MsgSignature><![CDATA[<%-signature%>]]></MsgSignature>' +
'<TimeStamp><%-timestamp%></TimeStamp>' +
'<Nonce><![CDATA[<%-nonce%>]]></Nonce>' +
'</xml>';
var wrapTpl = '<xml>' + '<Encrypt><![CDATA[<%-encrypt%>]]></Encrypt>' + '<MsgSignature><![CDATA[<%-signature%>]]></MsgSignature>' + '<TimeStamp><%-timestamp%></TimeStamp>' + '<Nonce><![CDATA[<%-nonce%>]]></Nonce>' + '</xml>';

var encryptWrap = ejs.compile(wrapTpl);

/*!
* 将内容回复给微信的封装方法
*/
var reply = function (content, fromUsername, toUsername) {
var reply = function reply(content, fromUsername, toUsername) {
var info = {};
var type = 'text';
info.content = content || '';
Expand All @@ -158,7 +126,7 @@ var reply = function (content, fromUsername, toUsername) {
return compiled(info);
};

var reply2CustomerService = function (fromUsername, toUsername, kfAccount) {
var reply2CustomerService = function reply2CustomerService(fromUsername, toUsername, kfAccount) {
var info = {};
info.msgType = 'transfer_customer_service';
info.createTime = new Date().getTime();
Expand Down Expand Up @@ -286,19 +254,155 @@ wechat.prototype.middleware = function (handle) {
} else {
var wrap = {};
wrap.encrypt = that.cryptor.encrypt(replyMessageXml);
wrap.nonce = parseInt((Math.random() * 100000000000), 10);
wrap.nonce = parseInt(Math.random() * 100000000000, 10);
wrap.timestamp = new Date().getTime();
wrap.signature = that.cryptor.getSignature(wrap.timestamp, wrap.nonce, wrap.encrypt);
this.body = encryptWrap(wrap);
}

this.type = 'application/xml';

} else {
this.status = 501;
this.body = 'Not Implemented';
}
};
};

module.exports = wechat;
wechat.prototype.middleware2 = function (handle) {
var that = this;
if (this.encodingAESKey) {
that.cryptor = new WXBizMsgCrypt(this.token, this.encodingAESKey, this.appid);
}
return function () {
var _ref = _asyncToGenerator(function* (ctx, next) {
var query = ctx.query;
// 加密模式
var encrypted = !!(query.encrypt_type && query.encrypt_type === 'aes' && query.msg_signature);
var timestamp = query.timestamp;
var nonce = query.nonce;
var echostr = query.echostr;
var method = ctx.method;

if (method === 'GET') {
var valid = false;
if (encrypted) {
var signature = query.msg_signature;
valid = signature === that.cryptor.getSignature(timestamp, nonce, echostr);
} else {
// 校验
valid = query.signature === getSignature(timestamp, nonce, that.token);
}
if (!valid) {
ctx.status = 401;
ctx.body = 'Invalid signature';
} else {
if (encrypted) {
var decrypted = that.cryptor.decrypt(echostr);
// TODO 检查appId的正确性
ctx.body = decrypted.message;
} else {
ctx.body = echostr;
}
}
} else if (method === 'POST') {
if (!encrypted) {
// 校验
if (query.signature !== getSignature(timestamp, nonce, that.token)) {
ctx.status = 401;
ctx.body = 'Invalid signature';
return;
}
}
// 取原始数据
var xml = yield getRawBody(ctx.req, {
length: ctx.length,
limit: '1mb',
encoding: ctx.charset
});

ctx.weixin_xml = xml;
// 解析xml
var result = yield parseXML2(xml);
var formated = formatMessage(result.xml);
if (encrypted) {
var encryptMessage = formated.Encrypt;
if (query.msg_signature !== that.cryptor.getSignature(timestamp, nonce, encryptMessage)) {
ctx.status = 401;
ctx.body = 'Invalid signature';
return;
}
var decryptedXML = that.cryptor.decrypt(encryptMessage);
var messageWrapXml = decryptedXML.message;
if (messageWrapXml === '') {
ctx.status = 401;
ctx.body = 'Invalid signature';
return;
}
//var decodedXML = yield parseXML(messageWrapXml);
var decodedXML = yield parseXML2(messageWrapXml);
formated = formatMessage(decodedXML.xml);
}

// 挂载处理后的微信消息
ctx.weixin = formated;
yield handle(ctx, next);
/*
// 取session数据
if (ctx.sessionStore) {
ctx.wxSessionId = formated.FromUserName;
ctx.wxsession = yield ctx.sessionStore.get(ctx.wxSessionId);
if (!ctx.wxsession) {
ctx.wxsession = {};
ctx.wxsession.cookie = ctx.session.cookie;
}
}

// 业务逻辑处理
yield* handle.call(ctx);

// 更新session
if (ctx.sessionStore) {
if (!ctx.wxsession) {
if (ctx.wxSessionId) {
yield ctx.sessionStore.destroy(ctx.wxSessionId);
}
} else {
yield ctx.sessionStore.set(ctx.wxSessionId, ctx.wxsession);
}
}
*/
/*
* 假如服务器无法保证在五秒内处理并回复,可以直接回复空串。
* 微信服务器不会对此作任何处理,并且不会发起重试。
*/
if (ctx.body === '') {
return;
}

var replyMessageXml = reply(ctx.body, formated.ToUserName, formated.FromUserName);

if (!query.encrypt_type || query.encrypt_type === 'raw') {
ctx.body = replyMessageXml;
} else {
var wrap = {};
wrap.encrypt = that.cryptor.encrypt(replyMessageXml);
wrap.nonce = parseInt(Math.random() * 100000000000, 10);
wrap.timestamp = new Date().getTime();
wrap.signature = that.cryptor.getSignature(wrap.timestamp, wrap.nonce, wrap.encrypt);
ctx.body = encryptWrap(wrap);
}

ctx.type = 'application/xml';
} else {
ctx.status = 501;
ctx.body = 'Not Implemented';
}
});

return function (_x, _x2) {
return _ref.apply(this, arguments);
};
}();
};

module.exports = wechat;
43 changes: 37 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"description": "wechat api for co",
"main": "index.js",
"scripts": {
"build": "npm run build:lib",
"build:lib": "babel src --out-dir lib",
"test": "make test-all"
},
"repository": {
Expand All @@ -23,17 +25,46 @@
"raw-body": "*"
},
"devDependencies": {
"supertest": "*",
"mocha": "*",
"should": "~3.0.0",
"babel-cli": "^6.3.17",
"babel-core": "^6.3.26",
"babel-eslint": "^5.0.0-beta9",
"babel-loader": "^6.2.0",
"babel-plugin-check-es2015-constants": "^6.3.13",
"babel-plugin-syntax-jsx": "^6.3.13",
"babel-plugin-transform-async-to-generator": "^6.16.0",
"babel-plugin-transform-decorators-legacy": "^1.2.0",
"babel-plugin-transform-es2015-arrow-functions": "^6.3.13",
"babel-plugin-transform-es2015-block-scoped-functions": "^6.3.13",
"babel-plugin-transform-es2015-block-scoping": "^6.3.13",
"babel-plugin-transform-es2015-classes": "^6.3.13",
"babel-plugin-transform-es2015-computed-properties": "^6.3.13",
"babel-plugin-transform-es2015-destructuring": "^6.3.13",
"babel-plugin-transform-es2015-for-of": "^6.3.13",
"babel-plugin-transform-es2015-function-name": "^6.3.13",
"babel-plugin-transform-es2015-literals": "^6.3.13",
"babel-plugin-transform-es2015-modules-commonjs": "^6.3.13",
"babel-plugin-transform-es2015-object-super": "^6.3.13",
"babel-plugin-transform-es2015-parameters": "^6.3.13",
"babel-plugin-transform-es2015-shorthand-properties": "^6.3.13",
"babel-plugin-transform-es2015-spread": "^6.3.13",
"babel-plugin-transform-es2015-sticky-regex": "^6.3.13",
"babel-plugin-transform-es2015-template-literals": "^6.3.13",
"babel-plugin-transform-es2015-unicode-regex": "^6.3.13",
"babel-plugin-transform-object-rest-spread": "^6.3.13",
"babel-plugin-transform-react-display-name": "^6.4.0",
"babel-plugin-transform-react-jsx": "^6.4.0",
"babel-register": "^6.3.13",
"coveralls": "*",
"expect.js": "*",
"istanbul-harmony": "*",
"koa": "0.13.*",
"koa-generic-session": "*",
"travis-cov": "*",
"coveralls": "*",
"mocha": "*",
"mocha-lcov-reporter": "*",
"muk": "*",
"rewire": "*",
"istanbul-harmony": "*"
"should": "~3.0.0",
"supertest": "*",
"travis-cov": "*"
}
}
Loading