From 49b651e6db69e726a3e713fc900c7996f1fc477d Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 20 Apr 2017 18:20:52 -0500 Subject: [PATCH 1/3] Update README.md code copy fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb69169..20f49a0 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ To write an app using the SDK postalCode: "78701", }; - client.createUser(userDate, function(errors, body, res) { + client.createUser(userData, function(errors, body, res) { if (errors) { console.log("Create User Failed"); console.log(errors); From 81ade3ad9455782ecc77ab853d74329bda1465ed Mon Sep 17 00:00:00 2001 From: aseveryn-epam Date: Mon, 3 Dec 2018 13:36:12 +0200 Subject: [PATCH 2/3] Check hyperwallet client response content type --- src/utils/ApiClient.js | 6 +++++ test/utils/ApiClient.spec.js | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/utils/ApiClient.js b/src/utils/ApiClient.js index 6292e9c..06349c7 100644 --- a/src/utils/ApiClient.js +++ b/src/utils/ApiClient.js @@ -125,6 +125,12 @@ export default class ApiClient { */ wrapCallback(callback = () => null) { return (err, res) => { + if (res !== undefined && !(res.type === "application/json" || res.type === "application/jose+json")) { + callback([{ + message: "Invalid Content-Type specified in Response Header", + }], res ? res.body : undefined, res); + return; + } if (!err) { callback(undefined, res.body, res); return; diff --git a/test/utils/ApiClient.spec.js b/test/utils/ApiClient.spec.js index a2631ea..e6a58f8 100644 --- a/test/utils/ApiClient.spec.js +++ b/test/utils/ApiClient.spec.js @@ -428,6 +428,7 @@ describe("utils/ApiClient", () => { const rawRes = { body: "test", status: 200, + type: "application/json", }; const callback = client.wrapCallback((err, body, res) => { @@ -452,6 +453,7 @@ describe("utils/ApiClient", () => { ], }, status: 404, + type: "application/json", }; const callback = client.wrapCallback((err, body, res) => { @@ -475,6 +477,7 @@ describe("utils/ApiClient", () => { const rawRes = { body: "test", status: 404, + type: "application/json", }; const callback = client.wrapCallback((err, body, res) => { @@ -489,5 +492,46 @@ describe("utils/ApiClient", () => { }); callback(new Error(), rawRes); }); + + it("should call callback with 'body' and 'res' and application/jose+json Content-Type", (cb) => { + const client = new ApiClient("test-username", "test-password", "test-server"); + + const rawRes = { + body: "test", + status: 200, + type: "application/jose+json", + }; + + const callback = client.wrapCallback((err, body, res) => { + expect(err).to.be.undefined(); + + body.should.be.equal("test"); + rawRes.should.be.deep.equal(res); + + cb(); + }); + callback(undefined, rawRes); + }); + + it("should call callback with static error message as 'errors', when Content-Type is wrong", (cb) => { + const client = new ApiClient("test-username", "test-password", "test-server"); + + const rawRes = { + body: "test", + status: 200, + type: "wrongContentType", + }; + + const callback = client.wrapCallback((err, body, res) => { + err.should.be.deep.equal([{ + message: "Invalid Content-Type specified in Response Header", + }]); + body.should.be.equal("test"); + rawRes.should.be.deep.equal(res); + + cb(); + }); + callback(new Error(), rawRes); + }); }); }); From 325f042269865710791f627af1ab6132770e39ab Mon Sep 17 00:00:00 2001 From: aseveryn-epam Date: Fri, 21 Dec 2018 13:27:13 +0200 Subject: [PATCH 3/3] Check response content type header refactoring --- src/utils/ApiClient.js | 86 ++++++++++++++++++++---------------- src/utils/Encryption.js | 29 ++++++++++++ test/utils/ApiClient.spec.js | 64 ++++++++++++++++----------- 3 files changed, 116 insertions(+), 63 deletions(-) diff --git a/src/utils/ApiClient.js b/src/utils/ApiClient.js index 3d5b0c8..b2d83fb 100644 --- a/src/utils/ApiClient.js +++ b/src/utils/ApiClient.js @@ -84,12 +84,12 @@ export default class ApiClient { * @param {api-callback} callback - The callback for this call */ doPost(partialUrl, data, params, callback) { - let contentType = "json"; - let processResponse = this.wrapCallback(callback); + let contentType = "application/json"; + let accept = "application/json"; let requestDataPromise = new Promise((resolve) => resolve(data)); if (this.isEncrypted) { contentType = "application/jose+json"; - processResponse = this.processEncryptedResponse("POST", callback); + accept = "application/jose+json"; requestDataPromise = this.encryption.encrypt(data); } requestDataPromise.then((requestData) => { @@ -98,10 +98,10 @@ export default class ApiClient { .auth(this.username, this.password) .set("User-Agent", `Hyperwallet Node SDK v${this.version}`) .type(contentType) - .accept("json") + .accept(accept) .query(params) .send(requestData) - .end(processResponse); + .end(this.wrapCallback("POST", callback)); }).catch(() => callback("Failed to encrypt body for POST request", undefined, undefined)); } @@ -114,12 +114,12 @@ export default class ApiClient { * @param {api-callback} callback - The callback for this call */ doPut(partialUrl, data, params, callback) { - let contentType = "json"; - let processResponse = this.wrapCallback(callback); + let contentType = "application/json"; + let accept = "application/json"; let requestDataPromise = new Promise((resolve) => resolve(data)); if (this.isEncrypted) { contentType = "application/jose+json"; - processResponse = this.processEncryptedResponse("PUT", callback); + accept = "application/jose+json"; requestDataPromise = this.encryption.encrypt(data); } requestDataPromise.then((requestData) => { @@ -128,10 +128,10 @@ export default class ApiClient { .auth(this.username, this.password) .set("User-Agent", `Hyperwallet Node SDK v${this.version}`) .type(contentType) - .accept("json") + .accept(accept) .query(params) .send(requestData) - .end(processResponse); + .end(this.wrapCallback("PUT", callback)); }).catch(() => callback("Failed to encrypt body for PUT request", undefined, undefined)); } @@ -143,36 +143,49 @@ export default class ApiClient { * @param {api-callback} callback - The callback for this call */ doGet(partialUrl, params, callback) { - let contentType = "json"; + let contentType = "application/json"; + let accept = "application/json"; if (this.isEncrypted) { contentType = "application/jose+json"; + accept = "application/jose+json"; } request .get(`${this.server}/rest/v3/${partialUrl}`) .auth(this.username, this.password) .set("User-Agent", `Hyperwallet Node SDK v${this.version}`) .type(contentType) - .accept("json") + .accept(accept) .query(params) - .end(this.isEncrypted ? this.processEncryptedResponse("GET", callback) : this.wrapCallback(callback)); + .end(this.wrapCallback("GET", callback)); } /** * Wrap a callback to process possible API and network errors * + * @param {string} httpMethod - The http method that is currently processing * @param {api-callback} callback - The final callback * @returns {function(err: Object, res: Object)} - The super agent callback * * @private */ - wrapCallback(callback = () => null) { + wrapCallback(httpMethod, callback = () => null) { return (err, res) => { - this.processResponse(err, res, callback); + if (res && ((!this.isEncrypted && res.type !== "application/json") || (this.isEncrypted && res.type !== "application/jose+json"))) { + callback([{ + message: "Invalid Content-Type specified in Response Header", + }], res ? res.body : undefined, res); + return; + } + if (this.isEncrypted) { + this.processEncryptedResponse(httpMethod, err, res, callback); + } else { + this.processNonEncryptedResponse(err, res, callback); + } }; } /** - * Process response from server + * Process non encrypted response from server * * @param {Object} err - Error object * @param {Object} res - Response object @@ -180,13 +193,7 @@ export default class ApiClient { * * @private */ - processResponse(err, res, callback) { - if (res !== undefined && !(res.type === "application/json" || res.type === "application/jose+json")) { - callback([{ - message: "Invalid Content-Type specified in Response Header", - }], res ? res.body : undefined, res); - return; - } + processNonEncryptedResponse(err, res, callback) { if (!err) { callback(undefined, res.body, res); return; @@ -205,25 +212,30 @@ export default class ApiClient { } /** - * Makes decryption for encrypted response bodies + * Process encrypted response from server * * @param {string} httpMethod - The http method that is currently processing - * @param {api-callback} callback - The callback method to be invoked after decryption + * @param {Object} err - Error object + * @param {Object} res - Response object + * @param {api-callback} callback - The final callback * * @private */ - processEncryptedResponse(httpMethod, callback) { - return (error, response) => { - if (!error) { - const responseBody = response.rawResponse ? response.rawResponse : response.text; - this.encryption.decrypt(responseBody) - .then((decryptedData) => { - callback(undefined, JSON.parse(decryptedData.payload.toString()), decryptedData); - }) - .catch(() => callback(`Failed to decrypt response for ${httpMethod} request`, responseBody, responseBody)); - } else { - this.processResponse(error, response, callback); + processEncryptedResponse(httpMethod, err, res, callback) { + if (!err) { + let responseBody = res.rawResponse ? res.rawResponse : res.text; + try { + responseBody = this.encryption.base64Encode(JSON.parse(res.text)); + } catch (e) { + // nothing to do } - }; + this.encryption.decrypt(responseBody) + .then((decryptedData) => { + callback(undefined, JSON.parse(decryptedData.payload.toString()), decryptedData); + }) + .catch(() => callback(`Failed to decrypt response for ${httpMethod} request`, responseBody, responseBody)); + } else { + this.processNonEncryptedResponse(err, res, callback); + } } } diff --git a/src/utils/Encryption.js b/src/utils/Encryption.js index 13f2458..ebe3d98 100644 --- a/src/utils/Encryption.js +++ b/src/utils/Encryption.js @@ -310,4 +310,33 @@ export default class Encryption { callback(!error && response.statusCode === 200); }); } + + /** + * Convert encrypted string to array of Buffer + * + * @param {string} encryptedBody - Encrypted body to be decoded + */ + base64Decode(encryptedBody) { + const parts = encryptedBody.split("."); + const decodedParts = []; + parts.forEach(elem => { + decodedParts.push(jose.util.base64url.decode(elem)); + }); + const decodedBody = {}; + decodedBody.parts = decodedParts; + return decodedBody; + } + + /** + * Convert array of Buffer to encrypted string + * + * @param {string} decodedBody - Array of Buffer to be decoded to encrypted string + */ + base64Encode(decodedBody) { + const encodedParts = []; + decodedBody.parts.forEach(part => { + encodedParts.push(jose.util.base64url.encode(Buffer.from(JSON.parse(JSON.stringify(part)).data))); + }); + return encodedParts.join("."); + } } diff --git a/test/utils/ApiClient.spec.js b/test/utils/ApiClient.spec.js index 9fb7e1f..1d2bb94 100644 --- a/test/utils/ApiClient.spec.js +++ b/test/utils/ApiClient.spec.js @@ -176,10 +176,12 @@ describe("utils/ApiClient", () => { .filteringPath(() => "/") .matchHeader("Authorization", authHeader) .matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`) - .matchHeader("Accept", "application/json") + .matchHeader("Accept", "application/jose+json") .matchHeader("Content-Type", "application/jose+json") .post("/", /.+/) - .reply(201, encryptedBody); + .reply(200, encryption.base64Decode(encryptedBody), { + "Content-Type": "application/jose+json", + }); clientWithEncryption.doPost("test", { message: "Test message" }, {}, (err, body, res) => { expect(err).to.be.undefined(); @@ -376,10 +378,10 @@ describe("utils/ApiClient", () => { .filteringPath(() => "/") .matchHeader("Authorization", authHeader) .matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`) - .matchHeader("Accept", "application/json") + .matchHeader("Accept", "application/jose+json") .matchHeader("Content-Type", "application/jose+json") .put("/", /.+/) - .reply(201, encryptedBody); + .reply(201, encryption.base64Decode(encryptedBody), { "Content-Type": "application/jose+json" }); clientWithEncryption.doPut("test", { message: "Test message" }, {}, (err, body, res) => { expect(err).to.be.undefined(); @@ -448,10 +450,10 @@ describe("utils/ApiClient", () => { .filteringPath(() => "/") .matchHeader("Authorization", authHeader) .matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`) - .matchHeader("Accept", "application/json") + .matchHeader("Accept", "application/jose+json") .matchHeader("Content-Type", "application/jose+json") .put("/", /.+/) - .reply(201, encryptedBody); + .reply(201, encryption.base64Decode(encryptedBody), { "Content-Type": "application/jose+json" }); clientWithEncryption.doPut("test", { message: "Test message" }, {}, (err, body, res) => { err.should.be.deep.equal("Failed to decrypt response for PUT request"); @@ -478,7 +480,7 @@ describe("utils/ApiClient", () => { .filteringPath(() => "/") .matchHeader("Authorization", authHeader) .matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`) - .matchHeader("Accept", "application/json") + .matchHeader("Accept", "application/jose+json") .matchHeader("Content-Type", "application/jose+json") .put("/", /.+/) .reply(404, { @@ -486,7 +488,7 @@ describe("utils/ApiClient", () => { "test1", "test2", ], - }); + }, { "Content-Type": "application/jose+json" }); clientWithEncryption.doPut("test", { message: "Test message" }, {}, (err, body, res) => { err.should.be.deep.equal([ @@ -644,9 +646,9 @@ describe("utils/ApiClient", () => { .filteringPath(() => "/") .matchHeader("Authorization", authHeader) .matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`) - .matchHeader("Accept", "application/json") + .matchHeader("Accept", "application/jose+json") .get("/") - .reply(200, encryptedBody); + .reply(200, encryption.base64Decode(encryptedBody), { "Content-Type": "application/jose+json" }); clientWithEncryption.doGet("test", {}, (err, body, res) => { expect(err).to.be.undefined(); @@ -690,7 +692,7 @@ describe("utils/ApiClient", () => { type: "application/json", }; - const callback = client.wrapCallback((err, body, res) => { + const callback = client.wrapCallback("POST", (err, body, res) => { expect(err).to.be.undefined(); body.should.be.equal("test"); @@ -715,7 +717,7 @@ describe("utils/ApiClient", () => { type: "application/json", }; - const callback = client.wrapCallback((err, body, res) => { + const callback = client.wrapCallback("POST", (err, body, res) => { err.should.be.deep.equal(["test1", "test2"]); body.should.be.deep.equal({ errors: [ @@ -739,7 +741,7 @@ describe("utils/ApiClient", () => { type: "application/json", }; - const callback = client.wrapCallback((err, body, res) => { + const callback = client.wrapCallback("POST", (err, body, res) => { err.should.be.deep.equal([{ message: "Could not communicate with test-server", code: "COMMUNICATION_ERROR", @@ -753,23 +755,33 @@ describe("utils/ApiClient", () => { }); it("should call callback with 'body' and 'res' and application/jose+json Content-Type", (cb) => { - const client = new ApiClient("test-username", "test-password", "test-server"); + const clientPath = path.join(__dirname, "..", "resources", "private-jwkset1"); + const hwPath = path.join(__dirname, "..", "resources", "public-jwkset1"); + const clientWithEncryption = new ApiClient("test-username", "test-password", "https://test-server", { + clientPrivateKeySetPath: clientPath, + hyperwalletKeySetPath: hwPath, + }); - const rawRes = { - body: "test", - status: 200, - type: "application/jose+json", + const encryption = new Encryption(clientPath, hwPath); + const testMessage = { + message: "Test message", }; - const callback = client.wrapCallback((err, body, res) => { - expect(err).to.be.undefined(); - - body.should.be.equal("test"); - rawRes.should.be.deep.equal(res); + encryption.encrypt(testMessage).then((encryptedBody) => { + const callback = clientWithEncryption.wrapCallback("POST", (err, body, res) => { + expect(err).to.be.undefined(); + expect(res).not.to.be.undefined(); + body.should.be.deep.equal(testMessage); - cb(); + cb(); + }); + const rawRes = { + text: JSON.stringify(encryption.base64Decode(encryptedBody)), + status: 200, + type: "application/jose+json", + }; + callback(undefined, rawRes); }); - callback(undefined, rawRes); }); it("should call callback with static error message as 'errors', when Content-Type is wrong", (cb) => { @@ -781,7 +793,7 @@ describe("utils/ApiClient", () => { type: "wrongContentType", }; - const callback = client.wrapCallback((err, body, res) => { + const callback = client.wrapCallback("POST", (err, body, res) => { err.should.be.deep.equal([{ message: "Invalid Content-Type specified in Response Header", }]);