mirror of
https://github.com/marcogll/passkit-generator.git
synced 2026-03-15 21:25:26 +00:00
Moved completely webserver activities from index.js to server.js
Replaced `RequestHandler` with `generatePass`. Now `generatePass` function returns JSON objects containing the pass stream or, otherwise, the error structure.
This commit is contained in:
236
index.js
236
index.js
@@ -11,6 +11,7 @@ const supportedTypesOfPass = /(boardingPass|eventTicket|coupon|generic|storeCard
|
|||||||
const Certificates = {
|
const Certificates = {
|
||||||
status: false
|
status: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const Configuration = {
|
const Configuration = {
|
||||||
passModelsDir: null,
|
passModelsDir: null,
|
||||||
output: {
|
output: {
|
||||||
@@ -21,12 +22,12 @@ const Configuration = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
Apply a filter to arg0 to remove hidden files names (starting with dot)
|
Apply a filter to arg0 to remove hidden files names (starting with dot)
|
||||||
@function removeDotFiles
|
@function removeHiddenFiles
|
||||||
@params {[String]} from - list of file names
|
@params {[String]} from - list of file names
|
||||||
@return {[String]}
|
@return {[String]}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function removeDotFiles(from) {
|
function removeHiddenFiles(from) {
|
||||||
return from.filter(e => e.charAt(0) !== ".");
|
return from.filter(e => e.charAt(0) !== ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +65,6 @@ function loadConfiguration(setup) {
|
|||||||
let docStruct = {};
|
let docStruct = {};
|
||||||
|
|
||||||
async.concat(certPaths, fs.readFile, function(err, contents) {
|
async.concat(certPaths, fs.readFile, function(err, contents) {
|
||||||
// contents is a Buffer array
|
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
@@ -235,123 +234,127 @@ function editPassStructure(options, passBuffer) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequestHandler(request, response) {
|
/**
|
||||||
if (!Certificates.status) {
|
Creates a pass with the passed information
|
||||||
throw new Error("passkit requires initialization by calling .init() method.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!supportedTypesOfPass.test(request.params.type)) {
|
@function generatePass
|
||||||
// 😊
|
@params {Object} options - The options about the model to be used and override pass data,
|
||||||
response.set("Content-Type", "application/json");
|
@return {Promise} - A JSON structure containing the error or the stream of the generated pass.
|
||||||
response.status(418).send({ ecode: 418, status: false, message: `Model unsupported. Refer to https://apple.co/2KKcCrB for supported pass models.`});
|
*/
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.readdir(`${Configuration.passModelsDir}/${request.params.type}.pass`, function (err, files) {
|
function generatePass(options) {
|
||||||
/* Invalid path for Configuration.passModelsDir */
|
return new Promise(function(success, reject) {
|
||||||
if (err) {
|
if (!options.modelName || typeof options.modelName !== "string") {
|
||||||
// 😊
|
return reject({
|
||||||
response.set("Content-Type", "application/json");
|
status: false,
|
||||||
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.`});
|
error: {
|
||||||
return;
|
message: "A string model name must be provided in order to continue.",
|
||||||
|
ecode: 418
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let list = removeDotFiles(files);
|
fs.readdir(path.resolve(Configuration.passModelsDir, `${options.modelName}.pass`), function(err, files) {
|
||||||
|
if (err) {
|
||||||
if (!list.length) {
|
return reject({
|
||||||
// 😊
|
status: false,
|
||||||
response.set("Content-Type", "application/json");
|
error: {
|
||||||
response.status(418).send({ ecode: 418, status: false, message: `Model for type [${request.params.type}] has no contents. Refer to https://apple.co/2IhJr0Q`});
|
message: "Provided model name doesn't match with any model in the folder.",
|
||||||
return;
|
ecode: 418
|
||||||
}
|
|
||||||
|
|
||||||
if (!list.includes("pass.json")) {
|
|
||||||
// 😊
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let options = (request.method === "POST" ? request.body : (request.method === "GET" ? request.params : {}));
|
|
||||||
fs.readFile(path.resolve(Configuration.passModelsDir, `${request.params.type}.pass`, "pass.json"), {}, function _parsePassJSONBuffer(err, passStructBuffer) {
|
|
||||||
editPassStructure(filterPassOptions(options), passStructBuffer)
|
|
||||||
.then(function _afterJSONParse(passFileBuffer) {
|
|
||||||
// Manifest dictionary
|
|
||||||
let manifest = {};
|
|
||||||
let archive = archiver("zip");
|
|
||||||
|
|
||||||
archive.append(passFileBuffer, { name: "pass.json" });
|
|
||||||
|
|
||||||
manifest["pass.json"] = forge.md.sha1.create().update(passFileBuffer.toString("binary")).digest().toHex();
|
|
||||||
|
|
||||||
async.each(list, function getHashAndArchive(file, callback) {
|
|
||||||
if (/(manifest|signature|pass)/ig.test(file)) {
|
|
||||||
// skipping files
|
|
||||||
return callback();
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// adding the files to the zip - i'm not using .directory method because it adds also hidden files like .DS_Store on macOS
|
let list = removeHiddenFiles(files);
|
||||||
archive.file(`${Configuration.passModelsDir}/${request.params.type}.pass/${file}`, { name: file });
|
|
||||||
|
|
||||||
let hashFlow = forge.md.sha1.create();
|
if (!list.length) {
|
||||||
|
return reject({
|
||||||
fs.createReadStream(`${Configuration.passModelsDir}/${request.params.type}.pass/${file}`)
|
status: false,
|
||||||
.on("data", function(data) {
|
error: {
|
||||||
hashFlow.update(data.toString("binary"));
|
message: "Model provided matched but unitialized. Refer to https://apple.co/2IhJr0Q to fill the model correctly.",
|
||||||
})
|
ecode: 418
|
||||||
.on("error", function(e) {
|
|
||||||
return callback(e);
|
|
||||||
})
|
|
||||||
.on("end", function() {
|
|
||||||
manifest[file] = hashFlow.digest().toHex().trim();
|
|
||||||
return callback();
|
|
||||||
});
|
|
||||||
}, function end(error) {
|
|
||||||
if (error) {
|
|
||||||
throw new Error(`Unable to compile manifest. ${error}`);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
archive.append(Buffer.from(JSON.stringify(manifest), "utf8"), { name: "manifest.json" });
|
if (!list.includes("pass.json")) {
|
||||||
|
return reject({
|
||||||
|
status: false,
|
||||||
|
error: {
|
||||||
|
message: "I'm a teapot. How am I supposed to serve you pass without pass.json in the chosen model as tea without water?",
|
||||||
|
ecode: 418
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let signatureBuffer = createSignature(manifest);
|
fs.readFile(path.resolve(Configuration.passModelsDir, `${options.modelName}.pass`, "pass.json"), {}, function _parsePassJSONBuffer(err, passStructBuffer) {
|
||||||
|
editPassStructure(filterPassOptions(options.overrides), passStructBuffer)
|
||||||
|
.then(function _afterJSONParse(passFileBuffer) {
|
||||||
|
let manifest = {};
|
||||||
|
let archive = archiver("zip");
|
||||||
|
|
||||||
archive.append(signatureBuffer, { name: "signature" });
|
archive.append(passFileBuffer, { name: "pass.json" });
|
||||||
|
|
||||||
let passName = (request.query.name || request.body.name || request.params.type + (new Date()).toISOString().split('T')[0].replace(/-/ig, ""));
|
manifest["pass.json"] = forge.md.sha1.create().update(passFileBuffer.toString("binary")).digest().toHex();
|
||||||
|
|
||||||
response.set({
|
async.each(list, function getHashAndArchive(file, callback) {
|
||||||
"Content-type": "application/vnd.apple.pkpass",
|
if (/(manifest|signature|pass)/ig.test(file)) {
|
||||||
"Content-disposition": `attachment; filename=${passName}.pkpass`
|
// skipping files
|
||||||
});
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
if (Configuration.output.shouldWrite && !!Configuration.output.dir) {
|
// adding the files to the zip - i'm not using .directory method because it adds also hidden files like .DS_Store on macOS
|
||||||
// Memorize and then make it download
|
archive.file(`${Configuration.passModelsDir}/${options.modelName}.pass/${file}`, { name: file });
|
||||||
let wstreamOutputPass = fs.createWriteStream(path.resolve(Configuration.output.dir, `${passName}.pkpass`));
|
|
||||||
archive.pipe(wstreamOutputPass);
|
|
||||||
|
|
||||||
wstreamOutputPass.on("close", function() {
|
let hashFlow = forge.md.sha1.create();
|
||||||
response.status(201).download(path.resolve(Configuration.output.dir, `${passName}.pkpass`), `${passName}.pkpass`, {
|
|
||||||
cacheControl: false
|
fs.createReadStream(`${Configuration.passModelsDir}/${options.modelName}.pass/${file}`)
|
||||||
|
.on("data", function(data) {
|
||||||
|
hashFlow.update(data.toString("binary"));
|
||||||
|
})
|
||||||
|
.on("error", function(e) {
|
||||||
|
return callback(e);
|
||||||
|
})
|
||||||
|
.on("end", function() {
|
||||||
|
manifest[file] = hashFlow.digest().toHex().trim();
|
||||||
|
return callback();
|
||||||
|
});
|
||||||
|
}, function end(error) {
|
||||||
|
if (error) {
|
||||||
|
return reject({
|
||||||
|
status: false,
|
||||||
|
error: {
|
||||||
|
message: `Unable to compile manifest. ${error}`,
|
||||||
|
ecode: 418
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
archive.append(JSON.stringify(manifest), { name: "manifest.json" });
|
||||||
|
|
||||||
|
let signatureBuffer = createSignature(manifest);
|
||||||
|
archive.append(signatureBuffer, { name: "signature" });
|
||||||
|
|
||||||
|
let passStream = new stream.PassThrough();
|
||||||
|
archive.pipe(passStream);
|
||||||
|
archive.finalize().then(function() {
|
||||||
|
return success({
|
||||||
|
status: true,
|
||||||
|
content: passStream,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
});
|
||||||
// Streaming directly the buffer
|
})
|
||||||
archive.pipe(response);
|
.catch(function(err) {
|
||||||
response.status(201);
|
return reject({
|
||||||
}
|
status: false,
|
||||||
|
error: {
|
||||||
archive.finalize();
|
message: `pass.json Buffer is not a valid buffer. Unable to continue.\n${err}`,
|
||||||
|
ecode: 418
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
})
|
|
||||||
.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)
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -361,40 +364,29 @@ function init(configPath) {
|
|||||||
throw new Error("Initialization must be triggered only once.");
|
throw new Error("Initialization must be triggered only once.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let configPathResolved = path.resolve(__dirname, configPath);
|
if (!configPath || typeof configPath !== "object" || typeof configPath === "object" && !Object.keys(configPath).length) {
|
||||||
|
throw new Error(`Cannot initialize PassKit module. Param 0 expects a non-empty configuration object.`);
|
||||||
if (!configPath || fs.accessSync(configPathResolved) !== undefined) {
|
|
||||||
throw new Error(`Cannot load configuration from 'path' (${configPath}). File not existing or missing path.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let setup = require(configPathResolved);
|
|
||||||
|
|
||||||
let queue = [
|
let queue = [
|
||||||
new Promise(function(success, reject) {
|
new Promise(function(success, reject) {
|
||||||
fs.access(path.resolve(setup.models.dir), function(err) {
|
fs.access(path.resolve(configPath.models.dir), function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return reject("A valid pass model directory is required. Please provide one in the configuration file under voice 'models.dir'.")
|
return reject("A valid pass model directory is required. Please provide one in the configuration file under voice 'models.dir'.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return success(null);
|
return success(true);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
new Promise((success) => fs.access(path.resolve(setup.output.dir), success)),
|
loadConfiguration(configPath)
|
||||||
loadConfiguration(setup)
|
|
||||||
];
|
];
|
||||||
|
|
||||||
Promise.all(queue)
|
Promise.all(queue)
|
||||||
.then(function(results) {
|
.then(function(results) {
|
||||||
let paths = results.slice(0, 2);
|
let certs = results[1];
|
||||||
let certs = results[results.length-1];
|
|
||||||
|
|
||||||
if (!paths[0]) {
|
if (results[0]) {
|
||||||
Configuration.passModelsDir = setup.models.dir;
|
Configuration.passModelsDir = configPath.models.dir;
|
||||||
}
|
|
||||||
|
|
||||||
if (!paths[1] && setup.output.shouldWrite) {
|
|
||||||
Configuration.output.dir = setup.output.dir;
|
|
||||||
Configuration.output.shouldWrite = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Certificates.wwdr = certs[0];
|
Certificates.wwdr = certs[0];
|
||||||
@@ -407,4 +399,4 @@ function init(configPath) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init, RequestHandler };
|
module.exports = { init, generatePass };
|
||||||
|
|||||||
44
server.js
44
server.js
@@ -1,12 +1,14 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
const passkit = require("./index");
|
const passkit = require("./index");
|
||||||
|
const Configuration = require("./config.json");
|
||||||
|
|
||||||
passkit.init("./config.json");
|
passkit.init(Configuration);
|
||||||
|
|
||||||
const instance = express();
|
const instance = express();
|
||||||
|
|
||||||
instance.use(express.json());
|
instance.use(express.json());
|
||||||
|
|
||||||
instance.listen(80, "0.0.0.0", function(request, response) {
|
instance.listen(80, "0.0.0.0", function(request, response) {
|
||||||
console.log("Listening on 80");
|
console.log("Listening on 80");
|
||||||
});
|
});
|
||||||
@@ -15,5 +17,39 @@ instance.get("/", function (request, response) {
|
|||||||
response.send("Hello there!");
|
response.send("Hello there!");
|
||||||
});
|
});
|
||||||
|
|
||||||
instance.get("/gen/:type/",passkit.RequestHandler);
|
function manageRequest(request, response) {
|
||||||
instance.post("/gen/:type/", passkit.RequestHandler);
|
let passName = (request.query.name ||
|
||||||
|
request.body.name ||
|
||||||
|
request.params.name ||
|
||||||
|
request.query.modelName ||
|
||||||
|
request.body.modelName ||
|
||||||
|
request.params.modelName) + "_" + (new Date()).toISOString().split('T')[0].replace(/-/ig, "");
|
||||||
|
|
||||||
|
response.set({
|
||||||
|
"Content-type": "application/vnd.apple.pkpass",
|
||||||
|
"Content-disposition": `attachment; filename=${passName}.pkpass`
|
||||||
|
});
|
||||||
|
|
||||||
|
passkit.generatePass({
|
||||||
|
modelName: request.params.modelName || request.query.modelName,
|
||||||
|
overrides: {}
|
||||||
|
})
|
||||||
|
.then(function(result) {
|
||||||
|
result.content.pipe(response);
|
||||||
|
|
||||||
|
// Writing to an output source
|
||||||
|
if (Configuration.output.dir && Configuration.output.shouldWrite && !fs.accessSync(path.resolve(Configuration.output.dir))) {
|
||||||
|
let wstreamOutputPass = fs.createWriteStream(path.resolve(Configuration.output.dir, `${passName}.pkpass`));
|
||||||
|
result.content.pipe(wstreamOutputPass);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
|
response.set("Content-Type", "application/json");
|
||||||
|
response.status(418).send(err);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.get("/gen/:modelName?", manageRequest);
|
||||||
|
instance.post("/gen/:modelName?", manageRequest);
|
||||||
|
|||||||
Reference in New Issue
Block a user