Initial commit
This commit is contained in:
commit
471d666de2
17
.eslintrc
Normal file
17
.eslintrc
Normal file
@ -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"] }]
|
||||
}
|
||||
}
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
.DS_Store
|
||||
.env
|
||||
node_modules
|
||||
.projectile
|
||||
.vscode
|
||||
.idea
|
||||
/.vs/
|
||||
/bin/
|
||||
182
README.md
Normal file
182
README.md
Normal file
@ -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
|
||||
|
||||
13
config/index.js
Normal file
13
config/index.js
Normal file
@ -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)
|
||||
}
|
||||
23
config/supportedApps.js
Normal file
23
config/supportedApps.js
Normal file
@ -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;
|
||||
};
|
||||
92
license-server.njsproj
Normal file
92
license-server.njsproj
Normal file
@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<ProjectGuid>{8dfa872b-4368-4176-811a-6d3d9665e54b}</ProjectGuid>
|
||||
<ProjectHome />
|
||||
<ProjectView>ShowAllFiles</ProjectView>
|
||||
<StartupFile />
|
||||
<WorkingDirectory>.</WorkingDirectory>
|
||||
<OutputPath>.</OutputPath>
|
||||
<ProjectTypeGuids>{3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}</ProjectTypeGuids>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'" />
|
||||
<ItemGroup>
|
||||
<Content Include="package-lock.json" />
|
||||
<Content Include="package.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="backend" />
|
||||
<Folder Include="license-server" />
|
||||
<Folder Include="sql\" />
|
||||
<Folder Include="src\" />
|
||||
<Folder Include="src\db\" />
|
||||
<Folder Include="src\routes\" />
|
||||
<Folder Include="src\util\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="config.js">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="index.js">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="sql\reset-db.js" />
|
||||
<Compile Include="src\util\licenseUtil.js">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="src\db\pool.js" />
|
||||
<Compile Include="src\index.js">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="src\routes\index.js">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<!--Do not delete the following Import Project. While this appears to do nothing it is a marker for setting TypeScript properties before our import that depends on them.-->
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets" Condition="False" />
|
||||
<Import Project="$(VSToolsPath)\Node.js Tools\Microsoft.NodejsTools.targets" />
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
|
||||
<WebProjectProperties>
|
||||
<UseIIS>False</UseIIS>
|
||||
<AutoAssignPort>True</AutoAssignPort>
|
||||
<DevelopmentServerPort>0</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:48022/</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
<UseCustomServer>True</UseCustomServer>
|
||||
<CustomServerUrl>http://localhost:1337</CustomServerUrl>
|
||||
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
||||
</WebProjectProperties>
|
||||
</FlavorProperties>
|
||||
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
|
||||
<WebProjectProperties>
|
||||
<StartPageUrl>
|
||||
</StartPageUrl>
|
||||
<StartAction>CurrentPage</StartAction>
|
||||
<AspNetDebugging>True</AspNetDebugging>
|
||||
<SilverlightDebugging>False</SilverlightDebugging>
|
||||
<NativeDebugging>False</NativeDebugging>
|
||||
<SQLDebugging>False</SQLDebugging>
|
||||
<ExternalProgram>
|
||||
</ExternalProgram>
|
||||
<StartExternalURL>
|
||||
</StartExternalURL>
|
||||
<StartCmdLineArguments>
|
||||
</StartCmdLineArguments>
|
||||
<StartWorkingDirectory>
|
||||
</StartWorkingDirectory>
|
||||
<EnableENC>False</EnableENC>
|
||||
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
|
||||
</WebProjectProperties>
|
||||
</FlavorProperties>
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
25
license-server.sln
Normal file
25
license-server.sln
Normal file
@ -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
|
||||
1421
package-lock.json
generated
Normal file
1421
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@ -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"
|
||||
}
|
||||
55
sql/reset-db.js
Normal file
55
sql/reset-db.js
Normal file
@ -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)
|
||||
}
|
||||
)
|
||||
301
sql/schema.sql
Normal file
301
sql/schema.sql
Normal file
@ -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;
|
||||
9
src/db/pool.js
Normal file
9
src/db/pool.js
Normal file
@ -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()
|
||||
71
src/index.js
Normal file
71
src/index.js
Normal file
@ -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
|
||||
57
src/middleware.js
Normal file
57
src/middleware.js
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/routes/index.js
Normal file
105
src/routes/index.js
Normal file
@ -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
|
||||
41
src/util/index.js
Normal file
41
src/util/index.js
Normal file
@ -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
|
||||
}
|
||||
243
src/util/licenseUtil.js
Normal file
243
src/util/licenseUtil.js
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user