commit 471d666de2a80148662bd816b30e2ae5409705fe Author: Peter Sykora Date: Tue Mar 27 23:07:46 2018 +0200 Initial commit diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..d51a075 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,17 @@ +{ + "extends": ["eslint:recommended", "react-app"], + "parserOptions": { + "ecmaVersion": 8, + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } + }, + "plugins": ["react"], + "env": { + "es6": true, + "node": true + }, + "rules": { + "no-console": ["error", { "allow": ["log", "warn", "error"] }] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6de1c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +.env +node_modules +.projectile +.vscode +.idea +/.vs/ +/bin/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ca6c26 --- /dev/null +++ b/README.md @@ -0,0 +1,182 @@ +# License server + +This is reference implementation of license server that allows per-machine software licensing while it also manages software and data updates for the +client machines. The protocol is described below. + +## Preactivation request + +When application is run for first time it is not activated. First it sends its system params (Fletcher64 hashes) to the server to check +if the computer has not been preactivated + +```http +POST /activate0 HTTP/1.1 +``` + +Example request body: +```json +{ + "appId": "coc", + "systemParams": { + "biosSerialNum": "8690a8fb436070a9", + "computerUUID": "13cfc3b6f8f7fdd2", + "diskSerialNum": "63a58b9728485155", + "nicMac": "4b2856a1e9e8f43e", + "osId": "ec4fe2f3023d1f21" + } +} +``` + +The server checks if the system is preactivated by checking if any of the system parameters exists in `PreactivationParams` table. +For example for license `XXXX-YYYY` there can be single entry for `biosSerialNum` parameters which matches the system of the requestor. +Therefore the activation will happen using the license number `XXXX-YYYY` and the system will response with license file that the client +system should store locally. We will describe the structure later. + +Example Response body: +```json +{ + "success": true, + "licenseFile": "fG2HUWhE5kT10Ono3qgO/pzHNU1KAnUlEszz3B1pP5FdSQlIukg3P3xUgfHDQX1OBuAFH68WeXe2T0YP1dbjSsAyDJfpUltJnncwMMLOkfR3YbvyAVNmScgASLWwyQxQAVID6GOQ2weVNo3tAcbj2Ted7rx0HL36seSzyY5xuV/SnfJN6q5acqyJmibQcsrPQLZBvIb00Cy9KENnkVFVH70kC406pKxVZ3Ghqg8vTxgBK6sdbwqd7XrpupcQ4frwwm/NPesCIBYRG+6C/9oMroQoPZ+NEzOYQq2PADOZgOSvaMp4FUNe30IqNckobQO7N2TmW4BJDKVzhG5OYQRfs+xvzG0lF5gOBtmBhDf3yStgJj++jc2pNwe0rDSP2J+Yjs+V8wepyuCJ08cfZOh1feqnzmAarzAD80W8wRFd9U2lHDsyFt7Ke/3aPX454jxqITn/Wu6MK0paX8YAATWWAONyUsWTdA3UkJm45gNHJibcQcZLwKErZEPr2XBNy8nrImHRGFFtr3KC" +} +``` + +If the Preactivation is not available for given client, the system will response with `{ success: false }` + +## Activation request + +If the system is not preactivated the client application should ask use to enter the license key (Or the license key should be somehow +received by the client system. The license key is 24 characters in base32 (RFC 4648) format. For simplicity it can be sent to user as +e.g. `T3HZ-IFAT-HLN5-2I57-HAGL-V24R` which is easy to read and enter manually. Dashes in between the characters are ignored. + +The client is supposed to send JSON request similar as in preactivation with just one parameter added `licenseNumber` which should be +uppercase without dashes so the server can directly query the database. + +### Example +```http +POST /activate HTTP/1.1 +Host: localhost:3000 +Accept: application/json +Content-Type: application/json +Content-Length: 219 +Charsets: utf-8 +``` + +Request data: +```json +{ + "appId": "coc", + "systemParams": { + "biosSerialNum": "8690a8fb436070a9", + "computerUUID": "13cfc3b6f8f7fdd2", + "diskSerialNum": "63a58b9728485155", + "nicMac": "4b2856a1e9e8f43e", + "osId": "ec4fe2f3023d1f21" + }, + "licenseNumber": "JK33BTBSBKSKV63YEVLMQMBZ" +} +``` + +Response: +``` +{ + "success": true, + "licenseFile": "CmiN19MDaeCtA4L+cqhrnL71GWjBqTA6cqP8pUyfwRY+r/s/CyODkgHnO9eg3yaLLNxnNFMOnZOtxgtz8hNMNUIsTAKzus068sz9dJhV2yLrmkvhi1KjEJdOua4ZXuKSzjGKxM+VFXokFfFTqxvVpPt5sMkwq9kG/cZSwpBw7POhR+ncHeF11jjkbKVUnVgjGq8EDHDQFANYAVB3qbo7PY9CG3Gm25nORMUMpqwKieadVmklBZYs09EUqqwxAxxpD44Hw2DaRwoaVMuKTC//wH+3oS3zoL2mx+panJ/HPCN7ZtdBje+v6HSlfUoHgCHpFrr0+9/YqvxAQBWz0Q8dSyzyHkuzEkb4Ob7uWSeVhmKJ1TfzX8CAchth9f4CLCuLdzmCpRVvIAM6ZS4o0t3n/3AAJAmBuKFr9OrHTnN1EgXUYB1TVjiHqvBXR1jEyvYyvkAC4CTel7/Y1LRK2y6mq5is/uLcuKyKpWItMr5p9/3qJAove7tZpT1KrNLYaIPGudIwcnYin8EBAK09uUrqRg==" +} +``` + +## Check for updates + +Another functionality that server supports is check for updates. +Client should send versions of its application modules (e.g. application, data) together with its activation ID, system parameters. +Server checks whether the system with given activation ID and system parameters is still valid and if yes it looks for updates +of the licensed modules. The typical request should look like this: + +```json +{ + "systemParams": { + "biosSerialNum": "8690a8fb436070a9", + "computerUUID": "13cfc3b6f8f7fdd2", + "diskSerialNum": "63a58b9728485155", + "nicMac": "4b2856a1e9e8f43e", + "osId": "ec4fe2f3023d1f21" + }, + "activationId": "f0d68f64-4bc5-33b0-6ab3-e9b446baea08", + "moduleVersions": { + "coc-testdata": 2 + } +} +``` + +The response contains `success: true` if the system is still activated with supplied activation number and system parameters. + +If the number of licensed modules has changed since the activation, the server can reactivate the installation and return back also new `licenseFile`. + +If there are newer versions available there should be list of updates attached. Here is the explanation of update parameters that are not obvious: + * flag - bit flag (bit 1: signals whether it is incremental update, bit 2: signals whether restart is required after update) + * checksum - SHA256 checksum of the zip file + +```json +{ + "success": true, + "moduleUpdates": [ + { + "moduleId": "coc-testdata", + "version": 3, + "flag": 0, + "checksum": "6c878854d349752eceb0d52658e8838c2ae3cca53962c942a276e8944da25731", + "updateUri": "http://localhost:3000/static/testsite-v3.zip", + "instPath": "data" + }, + { + "moduleId": "coc-testdata", + "version": 4, + "flag": 1, + "checksum": "a515353daae35dc1b3e9e06e52b95a53690984cc3172bb4e6b44c6b516afa040", + "updateUri": "http://localhost:3000/static/testsite-v4-incremental.zip", + "instPath": "data" + } + ] +} +``` + + + +## License file + +License file is zlib deflated and AES256-GCM encrypted of following structure + +```json +{ + "data": "{\"activationId\":\"e8cb99bc-9827-5c3a-b944-94ac97c81366\",\"appId\":\"coc\",\"systemParams\":{\"biosSerialNum\":\"8690a8fb436070a9\",\"computerUUID\":\"13cfc3b6f8f7fdd2\",\"diskSerialNum\":\"63a58b9728485155\",\"nicMac\":\"4b2856a1e9e8f43e\",\"osId\":\"ec4fe2f3023d1f21\"},\"licensedModules\":[\"coc-engine\",\"coc-testdata\"],\"nonce\":\"pSvFZJ8q3tqBzp0pLEmCSg==\"}", + "signature": "304c02240163a4b1a6e9a366672df1a17418ab44fb3471fefc4234b4220de8079d0a59ee3c05ce3502240346e0d66d388e3f6cef1cffe14c6be6930a858c72190359fa57aa755c5767b3688d9d88" +} +``` + +The signature is ECDSA+SHA256 signature of the serialized data node. The data node extracts further below: + +```json +{ + "activationId": "e8cb99bc-9827-5c3a-b944-94ac97c81366", + "appId": "coc", + "systemParams": { + "biosSerialNum": "8690a8fb436070a9", + "computerUUID": "13cfc3b6f8f7fdd2", + "diskSerialNum": "63a58b9728485155", + "nicMac": "4b2856a1e9e8f43e", + "osId": "ec4fe2f3023d1f21" + }, + "licensedModules": [ + "coc-engine", + "coc-testdata" + ] +} +``` + +As you can see the license file specifies activation number with all system parameters the license is activated to. +It also lists modules that are licensed for the given system. +Client system should save the license file received from the server localy and should upon every start check whether +the system parameters are the same as specified in the license file. The license file is protected from tempering by +ECDSA (public/private key cryptography) signature while the client only knows the public key of the server. + + +## Data model + diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..edc08a1 --- /dev/null +++ b/config/index.js @@ -0,0 +1,13 @@ +// Ensure require('dotenv').config() is run before this module is required + +exports.NODE_ENV = process.env.NODE_ENV || 'development' +exports.PORT = Number.parseInt(process.env.PORT, 10) || 3000 +exports.DATABASE_FILE = + process.env.DATABASE_URL || './data/license-server.db' + +// ////////////////////////////////////////////////////////// + +// Output config object in development to help with sanity-checking +if (exports.NODE_ENV === 'development' || exports.NODE_ENV === 'test') { + console.log(exports) +} diff --git a/config/supportedApps.js b/config/supportedApps.js new file mode 100644 index 0000000..7815d5c --- /dev/null +++ b/config/supportedApps.js @@ -0,0 +1,23 @@ +/** + * Pick parameters which are significant for including into license file + */ +exports.pickParams = function (appId, systemParams) { +// if (appId === 'coc') { + const appParams = ['biosSerialNum', 'osId', 'diskSerialNum', 'nicMac']; + const requiredParams = 2; +// } + + let resParams = {}; + console.log(systemParams) + for (paramId of appParams) { + if (systemParams.hasOwnProperty(paramId)) { + resParams[paramId] = systemParams[paramId]; + } + + if (Object.keys(resParams).length === requiredParams) { + return resParams; + } + } + + return null; +}; \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..1c404e2 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +require('./src').start() diff --git a/license-server.njsproj b/license-server.njsproj new file mode 100644 index 0000000..a5d2c49 --- /dev/null +++ b/license-server.njsproj @@ -0,0 +1,92 @@ + + + + Debug + 2.0 + {8dfa872b-4368-4176-811a-6d3d9665e54b} + + ShowAllFiles + + . + . + {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD} + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + + + + + + + + + + + Code + + + Code + + + + Code + + + + Code + + + Code + + + + + + + + + + + False + True + 0 + / + http://localhost:48022/ + False + True + http://localhost:1337 + False + + + + + + + CurrentPage + True + False + False + False + + + + + + + + + False + False + + + + + \ No newline at end of file diff --git a/license-server.sln b/license-server.sln new file mode 100644 index 0000000..53bec07 --- /dev/null +++ b/license-server.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2036 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "license-server", "license-server.njsproj", "{8DFA872B-4368-4176-811A-6D3D9665E54B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8DFA872B-4368-4176-811A-6D3D9665E54B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8DFA872B-4368-4176-811A-6D3D9665E54B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8DFA872B-4368-4176-811A-6D3D9665E54B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8DFA872B-4368-4176-811A-6D3D9665E54B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C030B8B7-C3CC-4C52-97CA-1C3852CB83D9} + EndGlobalSection +EndGlobal diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2b07261 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1421 @@ +{ + "name": "license-server", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "2.1.18", + "negotiator": "0.6.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, + "bytes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.5.0.tgz", + "integrity": "sha1-TJQj6i0lLCcMQbK97+/5u2tiwGo=" + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "co-body": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-5.1.1.tgz", + "integrity": "sha1-2XeB0eM0S6SoIP0YBr3fg0FQUjY=", + "requires": { + "inflation": "2.0.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.16" + } + }, + "compressible": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.13.tgz", + "integrity": "sha1-DRAgq5JLL9tNYnmHXH1tq6a6p6k=", + "requires": { + "mime-db": "1.33.0" + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-security-policy-builder": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.0.0.tgz", + "integrity": "sha512-j+Nhmj1yfZAikJLImCvPJFE29x/UuBi+/MWqggGGc515JKaZrjuei2RhULJmy0MsstW3E3htl002bwmBNMKr7w==" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookies": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.1.tgz", + "integrity": "sha1-fIphX1SBxhq58WyDNzG8uPZjuZs=", + "requires": { + "depd": "1.1.2", + "keygrip": "1.0.2" + } + }, + "copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" + }, + "dasherize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", + "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dns-prefetch-control": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz", + "integrity": "sha1-YN20V3dOF48flBXwyrsOhbCzALI=" + }, + "dont-sniff-mimetype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz", + "integrity": "sha1-WTKJDcn04vGeXrAqIAJuXl78j1g=" + }, + "dotenv": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", + "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "error-inject": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", + "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" + }, + "es6-denodeify": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-denodeify/-/es6-denodeify-0.1.5.tgz", + "integrity": "sha1-MdTV/pxVA+ElRgQ5MQ4WoqPznB8=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "expect-ct": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.1.0.tgz", + "integrity": "sha1-UnNWeN4YUwiQ2Ne5XwrGNkCVgJQ=" + }, + "frameguard": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.0.0.tgz", + "integrity": "sha1-e8rUae57lukdEs6zlZx4I1qScuk=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "guid": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/guid/-/guid-0.0.12.tgz", + "integrity": "sha1-kTfFKxhffeEkkLm+vMFmC5Al/gw=" + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "helmet": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.12.0.tgz", + "integrity": "sha512-CgkctpvreQLL6X3EL2Igs/92+75ZFIsrob9/Rdwf2hQCBGH/DxLk4xFPxAAl6jYnnus/YXfFEVXHEJf8TJTwlA==", + "requires": { + "dns-prefetch-control": "0.1.0", + "dont-sniff-mimetype": "1.0.0", + "expect-ct": "0.1.0", + "frameguard": "3.0.0", + "helmet-csp": "2.7.0", + "hide-powered-by": "1.0.0", + "hpkp": "2.0.0", + "hsts": "2.1.0", + "ienoopen": "1.0.0", + "nocache": "2.0.0", + "referrer-policy": "1.1.0", + "x-xss-protection": "1.1.0" + } + }, + "helmet-csp": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.7.0.tgz", + "integrity": "sha512-IGIAkWnxjRbgMXFA2/kmDqSIrIaSfZ6vhMHlSHw7jm7Gm9nVVXqwJ2B1YEpYrJsLrqY+w2Bbimk7snux9+sZAw==", + "requires": { + "camelize": "1.0.0", + "content-security-policy-builder": "2.0.0", + "dasherize": "2.0.0", + "lodash.reduce": "4.6.0", + "platform": "1.3.5" + } + }, + "hide-powered-by": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.0.0.tgz", + "integrity": "sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=" + }, + "hpkp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz", + "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=" + }, + "hsts": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.1.0.tgz", + "integrity": "sha512-zXhh/DqgrTXJ7erTN6Fh5k/xjMhDGXCqdYN3wvxUvGUQvnxcFfUd8E+6vLg/nk3ss1TYMb+DhRl25fYABioTvA==" + }, + "http-assert": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.3.0.tgz", + "integrity": "sha1-oxpc+IyHPsu1eWkH1NbxMujAHko=", + "requires": { + "deep-equal": "1.0.1", + "http-errors": "1.6.2" + } + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.4.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + } + } + }, + "humanize-number": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/humanize-number/-/humanize-number-0.0.2.tgz", + "integrity": "sha1-EcCvakcWQ2M1iFiASPF5lUFInBg=" + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "ienoopen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ienoopen/-/ienoopen-1.0.0.tgz", + "integrity": "sha1-NGpCj0dKrI9QzzeE6i0PFvYr2ms=" + }, + "inflation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha1-rTKXxVcGneqLz+ek+kkbdcXd65E=" + }, + "koa": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.5.0.tgz", + "integrity": "sha512-UkrbMW2mRNfoW/4I20knJEjtPAWCV3Iw6f4XdnPWjHsCN8iTeSh0eSutrYdL0fGF/G9on2eQ30EEQif0MarGJA==", + "requires": { + "accepts": "1.3.5", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookies": "0.7.1", + "debug": "3.1.0", + "delegates": "1.0.0", + "depd": "1.1.2", + "destroy": "1.0.4", + "error-inject": "1.0.0", + "escape-html": "1.0.3", + "fresh": "0.5.2", + "http-assert": "1.3.0", + "http-errors": "1.6.2", + "is-generator-function": "1.0.7", + "koa-compose": "4.0.0", + "koa-convert": "1.2.0", + "koa-is-json": "1.0.0", + "mime-types": "2.1.18", + "on-finished": "2.3.0", + "only": "0.0.2", + "parseurl": "1.3.2", + "statuses": "1.4.0", + "type-is": "1.6.16", + "vary": "1.1.2" + } + }, + "koa-better-static2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/koa-better-static2/-/koa-better-static2-1.0.2.tgz", + "integrity": "sha512-7tBuSaQb0QPmxEbp/mHERS0KRNdOt6LZcJLc6hZSy4Vz66yddiCKqTb7RdQJK+kj768Kh0y8K8d1yY23mxJCFQ==", + "requires": { + "debug": "3.1.0", + "resolve-path": "1.4.0" + } + }, + "koa-bodyparser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.2.0.tgz", + "integrity": "sha1-vObgi8Zfhwm20fqpQRx/DYk4qlQ=", + "requires": { + "co-body": "5.1.1", + "copy-to": "2.0.1" + } + }, + "koa-bouncer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/koa-bouncer/-/koa-bouncer-6.0.4.tgz", + "integrity": "sha512-fFtjDqebaXlruH5oh8TvCad91ltplU3nX45BL5Tmzb/jXYAb0zq39hfZjwOJvKJqS6NAPmzJH7JnyFgTKzjWZA==", + "requires": { + "better-assert": "1.0.2", + "debug": "3.1.0", + "lodash": "4.17.5", + "validator": "9.4.1" + } + }, + "koa-compose": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.0.0.tgz", + "integrity": "sha1-KAClE9nDYe8NY4UrA45Pby1adzw=" + }, + "koa-compress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-compress/-/koa-compress-2.0.0.tgz", + "integrity": "sha1-e36ykhuEd0a14SK6n1zYpnHo6jo=", + "requires": { + "bytes": "2.5.0", + "compressible": "2.0.13", + "koa-is-json": "1.0.0", + "statuses": "1.4.0" + } + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "4.6.0", + "koa-compose": "3.2.1" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "1.3.0" + } + } + } + }, + "koa-helmet": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/koa-helmet/-/koa-helmet-3.3.0.tgz", + "integrity": "sha512-kuUjVpCy8gjJkXO0JYVX8tYvX8vTVOCdogLNHuPczRaITbmd8r7EBcNrk8GzBPVQE0ZOwDUqCSaNZ6vZwsL+wA==", + "requires": { + "helmet": "3.12.0" + } + }, + "koa-is-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", + "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" + }, + "koa-logger": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/koa-logger/-/koa-logger-3.1.0.tgz", + "integrity": "sha512-+RQlE4GbPbcViuhwC9jSpdXRLRthzjsWpwu5kLa7qW3lbF/mBzVsjqdbhB/XosB15yGNN9FWSzf8UNW5S/Rgbw==", + "requires": { + "bytes": "2.5.0", + "chalk": "1.1.3", + "humanize-number": "0.0.2", + "passthrough-counter": "1.0.0" + } + }, + "koa-mount": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-3.0.0.tgz", + "integrity": "sha1-CMqzuD0xRC7Yt+dcVLGr65IuwZc=", + "requires": { + "debug": "2.6.9", + "koa-compose": "3.2.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "1.3.0" + } + } + } + }, + "koa-route": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/koa-route/-/koa-route-3.2.0.tgz", + "integrity": "sha1-dimLmaa8+p44yrb+XHmocz51i84=", + "requires": { + "debug": "3.1.0", + "methods": "1.1.2", + "path-to-regexp": "1.7.0" + } + }, + "koa-router": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-7.4.0.tgz", + "integrity": "sha512-IWhaDXeAnfDBEpWS6hkGdZ1ablgr6Q6pGdXCyK38RbzuH4LkUOpPqPw+3f8l8aTDrQmBQ7xJc0bs2yV4dzcO+g==", + "requires": { + "debug": "3.1.0", + "http-errors": "1.6.2", + "koa-compose": "3.2.1", + "methods": "1.1.2", + "path-to-regexp": "1.7.0", + "urijs": "1.19.1" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "1.3.0" + } + } + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nan": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "nocache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz", + "integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "passthrough-counter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passthrough-counter/-/passthrough-counter-1.0.0.tgz", + "integrity": "sha1-GWfZ5m2lcrXAI8eH2xEqOHqxZvo=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + } + }, + "platform": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", + "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + } + } + }, + "referrer-policy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.1.0.tgz", + "integrity": "sha1-NXdOtzW/UPtsB46DM0tHI1AgfXk=" + }, + "resolve-path": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=", + "requires": { + "http-errors": "1.6.2", + "path-is-absolute": "1.0.1" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "sqlite": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-2.9.1.tgz", + "integrity": "sha512-cu+Bkf5Kwc8JHoNr/iu3m+ZDzly5KIDc7omjwNimF7yZCTd+cIEzLpVXkYXJQFPz8GvHHrNvyqHIKE9JOvMzAw==", + "requires": { + "sqlite3": "3.1.13" + } + }, + "sqlite3": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-3.1.13.tgz", + "integrity": "sha512-JxXKPJnkZ6NuHRojq+g2WXWBt3M1G9sjZaYiHEWSTGijDM3cwju/0T2XbWqMXFmPqDgw+iB7zKQvnns4bvzXlw==", + "requires": { + "nan": "2.7.0", + "node-pre-gyp": "0.6.38" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.3" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.8", + "bundled": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "caseless": { + "version": "0.12.0", + "bundled": true + }, + "co": { + "version": "4.6.0", + "bundled": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true + }, + "extsprintf": { + "version": "1.3.0", + "bundled": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.4", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true + }, + "jsprim": { + "version": "1.4.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "mime-db": { + "version": "1.30.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.17", + "bundled": true, + "requires": { + "mime-db": "1.30.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true + }, + "node-pre-gyp": { + "version": "0.6.38", + "bundled": true, + "requires": { + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.2", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.2", + "semver": "5.4.1", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true + }, + "qs": { + "version": "6.4.0", + "bundled": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true + } + } + }, + "readable-stream": { + "version": "2.3.3", + "bundled": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true + }, + "semver": { + "version": "5.4.1", + "bundled": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.1", + "bundled": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "bundled": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "requires": { + "debug": "2.6.9", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.3.3", + "rimraf": "2.6.2", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.3", + "bundled": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "uuid": { + "version": "3.1.0", + "bundled": true + }, + "verror": { + "version": "1.10.0", + "bundled": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + } + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.18" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "urijs": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", + "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" + }, + "validator": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", + "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "x-xss-protection": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.1.0.tgz", + "integrity": "sha512-rx3GzJlgEeZ08MIcDsU2vY2B1QEriUKJTSiNHHUIem6eg9pzVOr2TL3Y4Pd6TMAM5D5azGjcxqI62piITBDHVg==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d72a7c5 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "license-server", + "version": "1.0.0", + "description": "License Server", + "main": "index.js", + "dependencies": { + "content-type": "^1.0.4", + "dotenv": "^5.0.1", + "es6-denodeify": "^0.1.5", + "guid": "0.0.12", + "koa": "^2.5.0", + "koa-better-static2": "^1.0.2", + "koa-bodyparser": "^4.2.0", + "koa-bouncer": "^6.0.4", + "koa-compress": "^2.0.0", + "koa-helmet": "^3.3.0", + "koa-logger": "^3.1.0", + "koa-mount": "^3.0.0", + "koa-route": "^3.2.0", + "koa-router": "^7.4.0", + "sqlite": "^2.9.1" + }, + "devDependencies": {}, + "scripts": { + "start": "node index.js", + "reset-db": "node ./sql/reset-db", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "engines": { + "node": ">=9.x" + }, + "author": "Peter Sykora", + "license": "ISC" +} diff --git a/sql/reset-db.js b/sql/reset-db.js new file mode 100644 index 0000000..2c09bdd --- /dev/null +++ b/sql/reset-db.js @@ -0,0 +1,55 @@ +require('dotenv').config() +// Node +const path = require('path') +const { promisify } = require('util') +const readFile = promisify(require('fs').readFile) +// 1st +const config = require('../config') +const { pool } = require('../src/db/pool') + +// ////////////////////////////////////////////////////////// + +// Sanity check: Ensure this isn't being run in production + +if (config.NODE_ENV !== 'development') { + throw new Error('[reset_db] May only reset database in development mode') +} + +// ////////////////////////////////////////////////////////// + +function slurpSql(filePath) { + const relativePath = '../sql/' + filePath + const fullPath = path.join(__dirname, relativePath) + return readFile(fullPath, 'utf8') +} + +async function seed() { + console.log('Resetting the database...') + + await (async () => { + const sql = await slurpSql('schema.sql') + console.log('-- Executing schema.sql...') + const conn = await pool; + console.log(conn) + console.log(await conn.get('SELECT 1')) + console.log(await conn.exec(sql)); + console.log(await conn.get('SELECT * FROM Activation')); + })() + +// await (async () => { +// const sql = await slurpSql('seeds.sql') +// console.log('-- Executing seeds.sql...') +// await pool._query(sql) +// })() +} + +seed().then( + () => { + console.log('Finished resetting db') + process.exit(0) + }, + err => { + console.error('Error:', err, err.stack) + process.exit(1) + } +) diff --git a/sql/schema.sql b/sql/schema.sql new file mode 100644 index 0000000..3ef5658 --- /dev/null +++ b/sql/schema.sql @@ -0,0 +1,301 @@ +-- +-- File generated with SQLiteStudio v3.1.1 on Tue Mar 13 19:16:57 2018 +-- +-- Text encoding used: UTF-8 +-- +PRAGMA foreign_keys = off; +BEGIN TRANSACTION; + +-- Table: Activation +DROP TABLE IF EXISTS Activation; + +CREATE TABLE Activation ( + activationId VARCHAR (40) PRIMARY KEY, + appId VARCHAR (20) NOT NULL, + licenseNum VARCHAR (24) NOT NULL, + activatedOn DATETIME NOT NULL, + deactivatedOn DATETIME, + FOREIGN KEY ( + appId, + licenseNum + ) + REFERENCES License (appId, + licenseNum) +); + + +-- Table: ActivationParams +DROP TABLE IF EXISTS ActivationParams; + +CREATE TABLE ActivationParams ( + activationId VARCHAR (40) NOT NULL, + paramId VARCHAR (20) NOT NULL, + paramValue VARCHAR NOT NULL, + flag INTEGER NOT NULL + DEFAULT (0), + PRIMARY KEY ( + activationId, + paramId + ), + FOREIGN KEY ( + activationId + ) + REFERENCES Activation (activationId) +); + + +-- Table: CheckLog +DROP TABLE IF EXISTS CheckLog; + +CREATE TABLE CheckLog ( + checkLogId VARCHAR (40) PRIMARY KEY + NOT NULL, + activationId VARCHAR (40) NOT NULL, + checkedOn DATETIME NOT NULL, + FOREIGN KEY ( + activationId + ) + REFERENCES Activation (activationId) +); + +-- Table: CheckLogParams +DROP TABLE IF EXISTS CheckLogParams; + +CREATE TABLE CheckLogParams ( + checkLogId VARCHAR (40) REFERENCES CheckLog (checkLogId) + NOT NULL, + paramId VARCHAR (20) NOT NULL, + paramValue VARCHAR NOT NULL, + PRIMARY KEY ( + checkLogId, + paramId + ) +); + + +-- Table: CheckLogVersion +DROP TABLE IF EXISTS CheckLogVersion; + +CREATE TABLE CheckLogVersion ( + checkLogId VARCHAR (40) NOT NULL + REFERENCES CheckLog (checkLogId), + moduleId VARCHAR (40) NOT NULL, + version INTEGER NOT NULL, + PRIMARY KEY ( + checkLogId, + moduleId + ) +); + + +-- Table: License +DROP TABLE IF EXISTS License; + +CREATE TABLE License ( + appId VARCHAR (20) NOT NULL, + licenseNum VARCHAR (24) NOT NULL, + customerId VARCHAR (20) NOT NULL, + PRIMARY KEY ( + appId, + licenseNum + ) +); + +INSERT INTO License ( + appId, + licenseNum, + customerId + ) + VALUES ( + 'coc', + 'T3HZIFATHLN52I57HAGLV24R', + '111' + ); + +INSERT INTO License ( + appId, + licenseNum, + customerId + ) + VALUES ( + 'coc', + 'JK33BTBSBKSKV63YEVLMQMBZ', + '111' + ); + + +-- Table: LicensedModule +DROP TABLE IF EXISTS LicensedModule; + +CREATE TABLE LicensedModule ( + appId VARCHAR (20) NOT NULL, + licenseNum VARCHAR (24) NOT NULL, + moduleId VARCHAR (40) NOT NULL, + PRIMARY KEY ( + appId, + licenseNum, + moduleId + ), + FOREIGN KEY ( + appId, + licenseNum + ) + REFERENCES License (appId, + licenseNum) +); + +INSERT INTO LicensedModule ( + appId, + licenseNum, + moduleId + ) + VALUES ( + 'coc', + 'JK33BTBSBKSKV63YEVLMQMBZ', + 'coc-engine' + ); + +INSERT INTO LicensedModule ( + appId, + licenseNum, + moduleId + ) + VALUES ( + 'coc', + 'JK33BTBSBKSKV63YEVLMQMBZ', + 'coc-testdata' + ); + +INSERT INTO LicensedModule ( + appId, + licenseNum, + moduleId + ) + VALUES ( + 'coc', + 'T3HZIFATHLN52I57HAGLV24R', + 'coc-testdata' + ); + +INSERT INTO LicensedModule ( + appId, + licenseNum, + moduleId + ) + VALUES ( + 'coc', + 'T3HZIFATHLN52I57HAGLV24R', + 'coc-engine' + ); + + +-- Table: ModuleUpdate +DROP TABLE IF EXISTS ModuleUpdate; + +CREATE TABLE ModuleUpdate ( + moduleId VARCHAR (40) NOT NULL, + version INTEGER NOT NULL, + checksum VARCHAR (32) NOT NULL, + updateUri VARCHAR NOT NULL, + instPath VARCHAR NOT NULL, + flag INTEGER NOT NULL + DEFAULT (0), + PRIMARY KEY ( + moduleId, + version + ) +); + +INSERT INTO ModuleUpdate ( + moduleId, + version, + checksum, + updateUri, + instPath, + flag + ) + VALUES ( + 'coc-testdata', + 2, + 'a515353daae35dc1b3e9e06e52b95a53690984cc3172bb4e6b44c6b516afa040', + 'http://localhost:3000/static/testsite-v2-incremental.zip', + 'data', + 1 + ); + +INSERT INTO ModuleUpdate ( + moduleId, + version, + checksum, + updateUri, + instPath, + flag + ) + VALUES ( + 'coc-testdata', + 1, + '6c878854d349752eceb0d52658e8838c2ae3cca53962c942a276e8944da25731', + 'http://localhost:3000/static/testsite-v1.zip', + 'data', + 0 + ); + + +-- Table: PreactivationParams +DROP TABLE IF EXISTS PreactivationParams; + +CREATE TABLE PreactivationParams ( + appId VARCHAR (20) NOT NULL, + licenseNum VARCHAR (24) NOT NULL, + paramId VARCHAR (20) NOT NULL, + paramValue VARCHAR (20) NOT NULL, + paramOrig VARCHAR (255), + PRIMARY KEY ( + appId, + licenseNum, + paramId + ), + FOREIGN KEY ( + appId, + licenseNum + ) + REFERENCES License (appId, + licenseNum) +); + +INSERT INTO PreactivationParams ( + appId, + licenseNum, + paramId, + paramValue, + paramOrig + ) + VALUES ( + 'coc', + 'T3HZIFATHLN52I57HAGLV24R', + 'biosSerialNum', + '8690a8fb436070a9', + 'R80CW80' + ); + + +-- View: ActiveActivationView +DROP VIEW IF EXISTS ActiveActivationView; +CREATE VIEW ActiveActivationView AS + SELECT * + FROM Activation + WHERE deactivatedOn IS NULL; + + +-- View: LastFullVersionView +DROP VIEW IF EXISTS LastFullVersionView; +CREATE VIEW LastFullVersionView AS + SELECT moduleId, + MAX(version) AS lastFullVersion + FROM ModuleUpdate + WHERE flag <> 1 + GROUP BY moduleId; + + +COMMIT TRANSACTION; +PRAGMA foreign_keys = on; diff --git a/src/db/pool.js b/src/db/pool.js new file mode 100644 index 0000000..d0938d8 --- /dev/null +++ b/src/db/pool.js @@ -0,0 +1,9 @@ +const sqlite = require('sqlite') + +const config = require('../../config') + +async function connect() { + return sqlite.open(config.DATABASE_FILE, { verbose:true, promise:Promise }) +} + +exports.pool = connect() diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..cef92ec --- /dev/null +++ b/src/index.js @@ -0,0 +1,71 @@ +// Load env vars from .env, always run this early +require('dotenv').config() + +// 3rd party +const debug = require('debug')('app:index') +const Koa = require('koa') +const helmet = require('koa-helmet') +const compress = require('koa-compress') +const serveStatic = require('koa-better-static2') +const logger = require('koa-logger') +const bodyParser = require('koa-bodyparser') +const bouncer = require('koa-bouncer') +const mount = require('koa-mount') + +// 1st party +const config = require('../config') +const mw = require('./middleware') + +// ////////////////////////////////////////////////////////// + +const app = new Koa() +app.poweredBy = false +app.proxy = config.TRUST_PROXY + +// ////////////////////////////////////////////////////////// +// Middleware +// ////////////////////////////////////////////////////////// + +app.use(helmet()) +app.use(compress()) +// TODO: You would set a high maxage on static assets if they had their hash in their filename. +// This project currently has no static asset build system setup. +app.use(mount('/static', serveStatic('static', { maxage: 0 }))) +app.use(logger()) +app.use(bodyParser()) + +/*var getRawBody = require('raw-body') +var contentType = require('content-type') +app.use(function* (next) { + console.log('data before ' + this.req.headers['content-length']); + + this.text = yield getRawBody(this.req, { + length: this.req.headers['content-length'], + limit: '1mb', + encoding: contentType.parse(this.req).parameters.charset + }) + console.log(this.text.toString()); + console.log('data after'); + yield next +}) */ + +app.use(mw.methodOverride()) // Must come after body parser +app.use(mw.removeTrailingSlash()) +app.use(bouncer.middleware()) +app.use(mw.handleBouncerValidationError()) // Must come after bouncer.middleware() + +// ////////////////////////////////////////////////////////// +// Routes +// ////////////////////////////////////////////////////////// + +app.use(require('./routes').routes()) + +// ////////////////////////////////////////////////////////// + +app.start = function (port = config.PORT) { + app.listen(port, () => { + console.log(`Listening on http://localhost:${port}`) + }) +} + +module.exports = app diff --git a/src/middleware.js b/src/middleware.js new file mode 100644 index 0000000..c9839e8 --- /dev/null +++ b/src/middleware.js @@ -0,0 +1,57 @@ +// 3rd +const debug = require('debug')('app:middleware') +const bouncer = require('koa-bouncer') +// 1st +const config = require('../config') + +exports.methodOverride = function() { + return async (ctx, next) => { + if (typeof ctx.request.body === 'undefined') { + throw new Error( + 'methodOverride middleware must be applied after the body is parsed and ctx.request.body is populated' + ) + } + + if (ctx.request.body && ctx.request.body._method) { + ctx.method = ctx.request.body._method.toUpperCase() + delete ctx.request.body._method + } + + return next() + } +} + +exports.removeTrailingSlash = function() { + return async (ctx, next) => { + if (ctx.path.length > 1 && ctx.path.endsWith('/')) { + ctx.redirect(ctx.path.slice(0, -1)) + return + } + + return next() + } +} + +exports.handleBouncerValidationError = function() { + return async (ctx, next) => { + try { + await next() + } catch (err) { + if (err instanceof bouncer.ValidationError) { + ctx.flash = { + message: ['danger', err.message || 'Validation error'], + // CAVEAT: Max cookie size is 4096 bytes. If the user sent us a + // body that exceeds that (for example, a large message), then + // the cookie will not get set (silently). + // TODO: Consider using localStorage to persist request bodies + // so that it scales. + params: ctx.request.body, + bouncer: err.bouncer, + } + return ctx.redirect('back') + } + + throw err + } + } +} diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000..b5c75ea --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,105 @@ +// 3rd party +const assert = require('better-assert') +const router = require('koa-router')() +const debug = require('debug')('app:routes:index') +// 1st party +const config = require('../../config') +const licenseUtil = require('../util/licenseUtil') + +// +// The index.js routes file is mostly a junk drawer for miscellaneous +// routes until it's accumulated enough routes to warrant a new +// routes/*.js module. +// + +function checkSystemParams(systemParams) { + if (systemParams !== null && typeof systemParams === 'object') { + if (Object.keys(systemParams).length > 1) { + let valid = true + Object.entries(systemParams).forEach(([key, value]) => { + if (typeof key !== 'string' || typeof value !== 'string') { + valid = false + } + }) + if (valid) { + return systemParams + } + } + } + throw new Error('Invalid parameters provided') +} + +function checkLicenseNumber(licenseNumber) { + if (licenseNumber !== null && typeof licenseNumber === 'string') { + if (licenseNumber.length === 24) { + return licenseNumber + } + } + throw new Error('Invalid license number') +} + +function checkActivationId(activationId) { + if (activationId !== null && typeof activationId === 'string') { + if (/^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$/.test(activationId)) { + return activationId + } + } + throw new Error('Invalid activation id') +} + +function checkAppId(appId) { + if (appId !== null && typeof appId === 'string') { + if (appId.length > 0) { + return (appId) + } + } + throw new Error('Invalid application id') +} + +function isInt(value) { + return !isNaN(value) && + parseInt(Number(value)) == value && + !isNaN(parseInt(value, 10)) +} + +function checkModuleVersions(moduleVersions) { + if (moduleVersions !== null && typeof moduleVersions === 'object') { + let valid = true; + Object.entries(moduleVersions).forEach(([key, value]) => { + if (typeof key !== 'string' || !isInt(value)) { + valid = false; + } + }) + if (valid) { + return moduleVersions; + } + } + throw new Error('Invalid parameters provided') +} + +router.post('/activate0', async ctx => { + console.log(ctx.request) + console.log(ctx.request.body) + ctx.body = await licenseUtil.preactivate( + checkAppId(ctx.request.body.appId), + checkSystemParams(ctx.request.body.systemParams) + ) +}) + +router.post('/activate', async ctx => { + ctx.body = await licenseUtil.activate( + checkAppId(ctx.request.body.appId), + checkLicenseNumber(ctx.request.body.licenseNumber), + checkSystemParams(ctx.request.body.systemParams) + ) +}) + +router.post('/check', async ctx => { + ctx.body = await licenseUtil.check( + checkSystemParams(ctx.request.body.systemParams), + checkActivationId(ctx.request.body.activationId), + checkModuleVersions(ctx.request.body.moduleVersions) + ) +}) + +module.exports = router \ No newline at end of file diff --git a/src/util/index.js b/src/util/index.js new file mode 100644 index 0000000..cf0e831 --- /dev/null +++ b/src/util/index.js @@ -0,0 +1,41 @@ +const crypto = require('crypto') +const zlib = require('zlib') + +const denodeify = require('es6-denodeify')(Promise) + +exports.generateLicenseFile = async function (activationId, appId, systemParams, licensedModules) { + const activationData = { + activationId, + appId, + systemParams, + licensedModules + }; + + const randomBytes = denodeify(crypto.randomBytes) + const deflateRaw = denodeify(zlib.deflateRaw) + + const algorithm = 'aes-256-gcm'; + const password = Buffer.from('e73db572349005f1c41979baf8166a0900745119fa096b9c3efbcee11ddd8b88', 'hex'); + const privateKey = '-----BEGIN EC PRIVATE KEY-----' + "\n" + + 'MIGAAgEBBCQBPIZnOt/mEsgtH3S9XZMGRuHkB5hYbMJ/BxcGmAc/pZLdxDWgBwYF' + "\n" + + 'K4EEABGhTANKAAQHyyrnJFywb+B0pcaVRHIOcEao3OtSMSJJZiluIMme1aE+20UA' + "\n" + + '0c0+2u+M6bMi072XrXLf8KudcAxihG/aqCqbVVZS6i10SSM=' + "\n" + + '-----END EC PRIVATE KEY-----'; + const nonce = await randomBytes(16) + activationData.nonce = nonce.toString('base64'); + let data = JSON.stringify(activationData); + const sign = crypto.createSign('SHA256'); + sign.write(data); + sign.end(); + const signature = sign.sign(privateKey, 'hex'); + data = JSON.stringify({ data, signature }); + compressed = await deflateRaw(data) + const iv = await randomBytes(16) + const cipher = crypto.createCipheriv(algorithm, password, iv); + let encrypted = cipher.update(compressed); + encrypted = Buffer.concat([encrypted, cipher.final()]); + const tag = cipher.getAuthTag(); + const output = Buffer.concat([iv, tag, encrypted]); + console.log(output.toString('hex')); + return output +} diff --git a/src/util/licenseUtil.js b/src/util/licenseUtil.js new file mode 100644 index 0000000..e846346 --- /dev/null +++ b/src/util/licenseUtil.js @@ -0,0 +1,243 @@ +const Guid = require('guid') + +const config = require('../../config') +const { pool } = require('../db/pool') +const { generateLicenseFile } = require('.') +const { pickParams } = require("../../config/supportedApps") + +let dbCache; + +const getDb = async function () { + if (!dbCache) { + dbCache = await pool + dbCache.on("trace", console.log) + } + return dbCache +} + +const findPreactivatedLicense = async function (appId, systemParams) { + console.log(appId); + console.log(systemParams); + const db = await getDb() + let counts = await Promise.all(Object.entries(systemParams).map(([param, val]) => + db.get('SELECT paramId, paramValue, COUNT(DISTINCT(licenseNum)) as count' + + ' FROM PreactivationParams' + + ' WHERE appId = ? AND paramId = ? AND paramValue = ?' + + ' GROUP BY paramId, paramValue' + + ' ORDER BY paramId, paramValue', + appId, param, val) + )) + + // If no licenses can be preactivated using any of given parameters + counts = counts + .filter((c) => (typeof c !== 'undefined' && c.count > 0)) + const totalCount = counts + .reduce((a, b) => a.count + b.count, 0) + if (totalCount < 1) { + return null; + } + + // Pick the most distinguishing parametere + const mostSignificantParam = counts + .sort((a, b) => (a.count - b.count))[0]; + + // Sanity check: Make sure that there are not too many licenses for the parameter + if (mostSignificantParam > 100) return null; + + const licensesToCheck = await db.all('SELECT licenseNum' + + ' FROM PreactivationParams' + + ' WHERE appId = ? AND paramId = ? AND paramValue = ?' + + ' ORDER BY licenseNum', + appId, mostSignificantParam.paramId, mostSignificantParam.paramValue) + + const availableLicenses = [] + for (let license of licensesToCheck) { + const paramPairs = await db.all('SELECT paramId, paramValue FROM PreactivationParams WHERE licenseNum = ?', license.licenseNum) + const allMatch = true + for (let paramPair of paramPairs) { + if (systemParams[paramPair.paramId] !== paramPair.paramValue) { + allMatch = false + break + } + } + if (allMatch) { + availableLicenses.push(license.licenseNum) + } + } + + return (availableLicenses.length > 0) ? availableLicenses[0] : null +} + +exports.activate = async function (appId, licenseNum, systemParams) { + const db = await getDb() + const activatedParams = await db.all('SELECT A1.paramId, A1.paramValue' + + ' FROM ActiveActivationView A0' + + ' INNER JOIN ActivationParams A1 on A1.activationId = A0.activationId' + + ' WHERE A0.appId = ? AND A0.licenseNum = ?' + + ' ORDER BY A1.paramId, A1.paramValue', + appId, licenseNum) + + if (activatedParams.length > 1) { + // Already activated! Check if system params still matches so we can reactivate + for (let activatedParam of activatedParams) { + if (systemParams[activatedParam.paramId] !== activatedParam.paramValue) { + return { success: false, reason: 'License already activated for different system' } + } + } + } + + const activationId = Guid.create(); + await db.run('UPDATE Activation SET deactivatedOn = date(?) WHERE licenseNum = ? and deactivatedOn IS NULL', 'now', licenseNum) + await db.run('INSERT INTO Activation (activationId, appId, licenseNum, activatedOn)' + + ' VALUES(?, ?, ?, date(?))', activationId.value, appId, licenseNum, 'now') + + const pickedParams = pickParams(appId, systemParams); + await Promise.all(Object.keys(systemParams).map((d) => { + flag = (d.paramId in pickedParams) ? 1 : 0 + return db.run('INSERT INTO ActivationParams (activationId, paramId, paramValue, flag)' + + ' VALUES (?,?,?,?)', activationId.value, d, systemParams[d], flag) + })) + + const licensedModulesRows = await db.all('SELECT moduleId' + + ' FROM LicensedModule' + + ' WHERE appId = ? AND licenseNum = ?' + + ' ORDER BY moduleId', + appId, licenseNum) + const licensedModules = licensedModulesRows.map((d) => d.moduleId) + + return { + success: true, + licenseFile: (await generateLicenseFile(activationId, appId, pickedParams, licensedModules)).toString("base64") + } +} + +exports.preactivate = async function(appId, systemParams) { + const preactivatedLicense = await findPreactivatedLicense(appId, systemParams) + if (preactivatedLicense === null) { + return { + success: false, + } + } + + return await exports.activate(appId, preactivatedLicense, systemParams); +} + +exports.check = async function (systemParams, activationId, moduleVersions) { + const db = await getDb() + const activatedParams = await db.all('SELECT A0.appId, A1.paramId, A1.paramValue' + + ' FROM ActiveActivationView A0' + + ' INNER JOIN ActivationParams A1 on A1.activationId = A0.activationId' + + ' WHERE A0.activationId = ?' + + ' ORDER BY A1.paramId, A1.paramValue', + activationId) + + if (activatedParams.length < 1) { + return { success: false, reason: 'Not active' } + } + + const checkLogId = Guid.create().value; + await db.run('INSERT INTO CheckLog (checkLogId, activationId, checkedOn)' + + ' VALUES(?, ?, date(?))', checkLogId, activationId, 'now') + await Promise.all(Object.keys(systemParams).map((d) => + db.run('INSERT INTO CheckLogParams (checkLogId, paramId, paramValue)' + + ' VALUES (?,?,?)', checkLogId, d, systemParams[d]))) + await Promise.all(Object.keys(moduleVersions).map((d) => + db.run('INSERT INTO CheckLogVersion (checkLogId, moduleId, version)' + + ' VALUES (?,?,?)', checkLogId, d, moduleVersions[d]))) + + + for (let activatedParam of activatedParams) { + if (systemParams[activatedParam.paramId] !== activatedParam.paramValue) { + return { success: false, reason: 'Invalid activation' } + } + } + + const lastModuleVersions = await db.all( + 'SELECT MU2.moduleId, MU2.version, MU2.flag, MU2.checksum, MU2.updateUri, MU2.instPath ' + + ' FROM (SELECT MU.moduleId, MAX(MU.version) as lastVersion' + + ' FROM ModuleUpdate MU' + + ' INNER JOIN LicensedModule LM ON LM.moduleId = MU.moduleId' + + ' INNER JOIN ActiveActivationView A ON A.appId = LM.appId AND A.licenseNum = LM.licenseNum' + + ' WHERE A.activationId = ?' + + ' GROUP BY MU.moduleId) LV' + + ' INNER JOIN ModuleUpdate MU2 ON MU2.moduleId = LV.moduleId AND MU2.version = LV.lastVersion' + + ' ORDER BY LV.moduleId', + activationId); + + let updateLicenseFile = (Object.keys(moduleVersions).length > lastModuleVersions.length); + const modulesToUpdate = []; + for (let m of lastModuleVersions) { + if (!(m.moduleId in moduleVersions)) { + if (Object.keys(moduleVersions).length) { + updateLicenseFile = true; + } + modulesToUpdate.push(m); + } else if(m.version > moduleVersions[m.moduleId]) { + modulesToUpdate.push({ + ...m, + currentVersion: moduleVersions[m.moduleId], + }) + } + } + + let updates = modulesToUpdate.filter((d) => !(d.flag & 1)) + const lastIncrementalUpdates = modulesToUpdate.filter((d) => d.flag & 1) + + if (lastIncrementalUpdates.length > 0) { + const conds = lastIncrementalUpdates.map((d) => { + let result = 'MU.moduleId = ?' + let params = [d.moduleId] + if (typeof (d.currentVersion) !== 'undefined') { + result += ' AND MU.version > ?' + params.push(d.currentVersion) + } + return { + cond: result, + params + } + }) + + const queryConds = conds.map((d) => d.cond).join(' OR ') + const queryParams = [].concat.apply([], conds.map((d) => d.params)) + + const incrementalUpdates = await db.all( + 'SELECT MU.moduleId, MU.version, MU.flag, MU.checksum, MU.updateUri, MU.instPath ' + + ' FROM ModuleUpdate MU' + + ' INNER JOIN LastFullVersionView LFV ON LFV.moduleId = MU.moduleId AND LFV.lastFullVersion <= MU.version' + + ' WHERE ' + queryConds + + ' ORDER BY MU.moduleId, MU.version', + queryParams) + + updates = updates.concat(incrementalUpdates) + } + + if (updateLicenseFile) { + // Update license file as some modules received license since last activation + + const licensedModulesRows = await db.all('SELECT LM.moduleId' + + ' FROM LicensedModule LM' + + ' INNER JOIN ActiveActivationView A ON A.appId = LM.appId AND A.licenseNum = LM.licenseNum' + + ' WHERE A.activationId = ?' + + ' ORDER BY LM.moduleId', + activationId) + const licensedModules = licensedModulesRows.map((d) => d.moduleId) + + let appId = null; + let activatedParamObj = {} + for (elm of activatedParams) { + activatedParamObj[elm.paramId] = elm.paramValue; + appId = elm.appId; + } + + return { + success: true, + licenseFile: (await generateLicenseFile(activationId, appId, activatedParamObj, licensedModules)).toString("base64"), + moduleUpdates: updates + } + } + + return { + success: true, + moduleUpdates: updates + } +}