From 37e3e69df6aa20f9cdd4a090a049f0e264772a52 Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Wed, 9 May 2018 18:12:40 +0200 Subject: [PATCH] Requests and serving update Added support for POST Requests with JSON HTTP Body and GET Requests with QueryString, Renamed res and req in 'response' and 'request', Added support for pass.json buffer retrieval, editing and direct compression in the zip file, Introduced function fileStreamToBuffer to simplify the retrivial of pass.json, Edited pass types names conforming to Apple guide --- .gitignore | 1 + index.js | 243 ++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 166 insertions(+), 78 deletions(-) diff --git a/.gitignore b/.gitignore index f393f22..d2c3534 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ certificates/passCertificate-exported.p12 certificates/passcertificate.pem certificates/passkey.pem certificates/source-AppleWWDRCA.cer +improvements.rtf diff --git a/index.js b/index.js index 817e9c3..8b1ce98 100644 --- a/index.js +++ b/index.js @@ -10,11 +10,13 @@ const async = require("async"); const _configuration = Object.freeze(require("./config.json")); const instance = express(); -const supportedTypesOfPass = /(boarding|event|coupon|generic|store)/i; +const supportedTypesOfPass = /(boardingPass|eventTicket|coupon|generic|storeCard)/i; const passModelsDir = _configuration.models.dir; const outputDir = _configuration.output.dir; const Certificates = _configuration.certificates; +instance.use(express.json()); + /** Apply a filter to arg0 to remove hidden files names (starting with dot) @function removeDotFiles @@ -30,6 +32,25 @@ function capitalizeFirst(str) { return str[0].toUpperCase()+str.slice(1); } +function fileStreamToBuffer(path, ...callbacks) { + let stream = fs.createReadStream(path); + let bufferArray = []; + + if (!path || typeof path !== "string") { + throw new Error("fileStreamToBuffer: Argument 0 is not provided or is not a string."); + } + + if (!callbacks || !callbacks.length) { + throw new Error("fileStreamToBuffer: Argument 1, at least one function must be provided."); + } + + stream + .on("data", chunk => bufferArray.push(chunk)) + .on("end", () => callbacks[0](Buffer.concat(bufferArray))) + // calling callbacks 0 or 1, based on the condition + .on("error", (e) => callbacks[Number(callbacks.length > 1)](e)); +} + /** Checks if the certificate and the key files originated from che .p12 file are available @@ -138,28 +159,68 @@ function generateManifest(fromObject, manifestUUID) { }); } -instance.listen(80, "0.0.0.0", function(req, res) { - console.log("Listening on 80"); -}); +function queryToOptions(query) { + // Some options are not supported since should be included inside the model + // Replace null with handlers to check the correctness of the values if needed. + // Handlers should contain check + const supportedOptions = { + serialNumber: null, + userInfo: null, + expirationDate: null, + locations: null, + authenticationToken: null, + barcode: null + }; -instance.get("/", function (req, res) { - res.send("Hello there!"); -}); + let options = {}; -instance.get("/gen/:type/", function (req, res) { - if (!supportedTypesOfPass.test(req.params.type)) { + Object.keys(supportedOptions).forEach(function(key) { + if (!!query[key]) { + if (!supportedOptions[key] || typeof supportedOptions[key] !== "function" || typeof supportedOptions[key] === "function" && supportedOptions[key](query[key])) { + options[key] = query[key]; + } + } + }); + + return options; +} + +function editPassStructure(object, passBuffer) { + + if (!object) { + return Promise.resolve(passBuffer); + } + + return new Promise(function(done, reject) { + try { + let passFile = JSON.parse(passBuffer.toString("utf8")); + + for (prop in object) { + passFile[prop] = object[prop]; + } + + return done(Buffer.from(JSON.stringify(passFile))); + } catch(e) { + return reject(e); + } + }); +} + + +function RequestHandler(request, response) { + if (!supportedTypesOfPass.test(request.params.type)) { // ๐Ÿ˜Š - res.set("Content-Type", "application/json"); - res.status(418).send({ ecode: 418, status: false, message: `Model unsupported. Refer to https://apple.co/2KKcCrB for supported pass models.`}); + response.set("Content-Type", "application/json"); + response.status(418).send({ ecode: 418, status: false, message: `Model unsupported. Refer to https://apple.co/2KKcCrB for supported pass models.`}); return; } - fs.readdir(`${passModelsDir}/${req.params.type}.pass`, {}, function (err, files) { + fs.readdir(`${passModelsDir}/${request.params.type}.pass`, function (err, files) { /* Invalid path for passModelsDir */ if (err) { // ๐Ÿ˜Š - res.set("Content-Type", "application/json"); - res.status(418).send({ ecode: 418, status: false, message: `Model not available for requested type [${res.params.type}]. Provide a folder with specified name and .pass extension.`}); + response.set("Content-Type", "application/json"); + response.status(418).send({ ecode: 418, status: false, message: `Model not available for request type [${request.params.type}]. Provide a folder with specified name and .pass extension.`}); return; } @@ -167,88 +228,114 @@ instance.get("/gen/:type/", function (req, res) { if (!list.length) { // ๐Ÿ˜Š - res.set("Content-Type", "application/json"); - res.status(418).send({ ecode: 418, status: false, message: `Model for type [${req.params.type}] has no contents. Refer to https://apple.co/2IhJr0Q `}); + response.set("Content-Type", "application/json"); + response.status(418).send({ ecode: 418, status: false, message: `Model for type [${request.params.type}] has no contents. Refer to https://apple.co/2IhJr0Q`}); return; } if (!list.includes("pass.json")) { // ๐Ÿ˜Š - res.set("Content-Type", "application/json"); - res.status(418).send({ ecode: 418, status: false, message: "I'm a tea pot. How am I supposed to serve you pass without pass.json in the chosen model as tea without water?" }); + response.set("Content-Type", "application/json"); + response.status(418).send({ ecode: 418, status: false, message: "I'm a teapot. How am I supposed to serve you pass without pass.json in the chosen model as tea without water?" }); return; } - // Manifest dictionary - let manifestRaw = {}; - let archive = archiver("zip"); + let options = (request.method === "POST" ? request.body : (request.method === "GET" ? request.params : {})); + fileStreamToBuffer(`${passModelsDir}/${request.params.type}.pass/pass.json`, function _returnBuffer(bufferResult) { + editPassStructure(queryToOptions(options), bufferResult).then(function _afterJSONParse(passFileBuffer) { + // Manifest dictionary + let manifestRaw = {}; + let archive = archiver("zip"); - async.each(list, function getHashAndArchive(file, callback) { - if (file !== "manifest.json" && file !== "signature") { - let passFileStream = fs.createReadStream(`${passModelsDir}/${req.params.type}.pass/${file}`); - let hashFlow = crypto.createHash("sha1"); + archive.append(passFileBuffer, { name: "pass.json" }); + manifestRaw["pass.json"] = crypto.createHash("sha1").update(passFileBuffer).digest("hex").trim(); - // adding the files to the zip - i'm not using .directory method because it adds also hidden files like .DS_Store on macOS - archive.file(`${passModelsDir}/${req.params.type}.pass/${file}`, { name: file }); - - passFileStream.on("data", function(data) { - hashFlow.update(data); - }); - - passFileStream.on("error", function(e) { - return callback(e); - }); - - passFileStream.on("end", function() { - manifestRaw[file] = hashFlow.digest("hex").trim(); - return callback(); - }); - } elseย { - // skipping files - return callback(); - } - }, function end(error) { - if (error) { - throw new Error(`Unable to compile manifest. ${error}`); - } - - let uuid = UUIDGen(); - - generateManifest(manifestRaw, uuid) - .then(function(manifestBuffer) { - - archive.append(manifestBuffer, { name: "manifest.json" }); - - generateManifestSignature(uuid) - .then(function(signatureBuffer) { - - if (!fs.existsSync("output")) { - fs.mkdirSync("output"); + async.each(list, function getHashAndArchive(file, callback) { + if (/(manifest|signature|pass)/ig.test(file)) { + // skipping files + return callback(); } - archive.append(signatureBuffer, { name: "signature" }); - let outputWS = fs.createWriteStream(`${outputDir}/${req.params.type}.pkpass`); + // adding the files to the zip - i'm not using .directory method because it adds also hidden files like .DS_Store on macOS + archive.file(`${passModelsDir}/${request.params.type}.pass/${file}`, { name: file }); - archive.pipe(outputWS); - archive.finalize(); + let hashFlow = crypto.createHash("sha1"); - outputWS.on("close", function() { - res.status(201).download(`${outputDir}/${req.params.type}.pkpass`, `${req.params.type}.pkpass`, { - cacheControl: false, - headers: { - "Content-type": "application/vnd.apple.pkpass", - "Content-length": fs.statSync(`${outputDir}/${req.params.type}.pkpass`).size - } - }); + fs.createReadStream(`${passModelsDir}/${request.params.type}.pass/${file}`) + .on("data", function(data) { + hashFlow.update(data); + }) + .on("error", function(e) { + return callback(e); + }) + .on("end", function() { + manifestRaw[file] = hashFlow.digest("hex").trim(); + return callback(); + }); + }, function end(error) { + if (error) { + throw new Error(`Unable to compile manifest. ${error}`); + } + + let uuid = UUIDGen(); + + generateManifest(manifestRaw, uuid) + .then(function(manifestBuffer) { + + archive.append(manifestBuffer, { name: "manifest.json" }); + + generateManifestSignature(uuid) + .then(function(signatureBuffer) { + + if (!fs.existsSync("output")) { + fs.mkdirSync("output"); + } + + archive.append(signatureBuffer, { name: "signature" }); + let outputWS = fs.createWriteStream(`${outputDir}/${request.params.type}.pkpass`); + + archive.pipe(outputWS); + archive.finalize(); + + outputWS.on("close", function() { + response.status(201).download(`${outputDir}/${request.params.type}.pkpass`, `${request.params.type}.pkpass`, { + cacheControl: false, + headers: { + "Content-type": "application/vnd.apple.pkpass", + "Content-length": fs.statSync(`${outputDir}/${request.params.type}.pkpass`).size + } + }); + }); + }) + .catch(function(buffer) { + throw buffer.toString(); + }); + }) + .catch(function(error) { + throw error; }); - }) - .catch(function(buffer) { - throw buffer.toString(); }); + }) - .catch(function(error) { - throw error; + .catch(function(err) { + // ๐Ÿ˜Š + response.set("Content-Type", "application/json"); + response.status(418).send({ ecode: 418, status: false, message: `Got error while parsing pass.json file: ${err}` }); + return; }); + }, function _error(e) { + console.log(e) }); }); +} + +instance.listen(80, "0.0.0.0", function(request, response) { + console.log("Listening on 80"); }); + +instance.get("/", function (request, response) { + response.send("Hello there!"); +}); + +instance.get("/gen/:type/", RequestHandler); +instance.post("/gen/:type/", RequestHandler);