@universis/signer-worker 1.0.5 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -3
- package/dist/SignerService.d.ts +72 -0
- package/dist/SignerServiceImpl.d.ts +61 -0
- package/dist/SignerWorker.d.ts +5 -24
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +341 -11
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +343 -9
- package/dist/index.js.map +1 -1
- package/dist/signerWorkerRouter.d.ts +1 -1
- package/jest.setup.js +23 -0
- package/package.json +18 -9
- package/src/SignerService.ts +339 -0
- package/src/SignerServiceImpl.ts +58 -0
- package/src/SignerWorker.ts +32 -76
- package/src/index.ts +3 -1
- package/src/signerWorkerRouter.ts +3 -2
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@universis/signer-worker",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Universis signer worker for server applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "
|
|
8
|
+
"test": "jest",
|
|
9
9
|
"build": "rimraf dist && rollup --config rollup.config.js"
|
|
10
10
|
},
|
|
11
11
|
"bin": "bin/signer-worker",
|
|
@@ -36,29 +36,38 @@
|
|
|
36
36
|
"@themost/xml": "^2"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"express": "^4.
|
|
39
|
+
"express": "^4.18.2",
|
|
40
40
|
"http-proxy-middleware": "^2.0.1",
|
|
41
|
-
"passport": "^0.
|
|
42
|
-
"superagent": "^
|
|
41
|
+
"passport": "^0.6.0",
|
|
42
|
+
"superagent": "^8.0.9",
|
|
43
|
+
"xotp": "^1.0.12"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
46
|
+
"@jest/globals": "^30.0.0",
|
|
45
47
|
"@rollup/plugin-typescript": "^8.3.0",
|
|
46
|
-
"@themost/
|
|
48
|
+
"@themost/common": "^2.11.1",
|
|
49
|
+
"@themost/data": "^2.18.8",
|
|
50
|
+
"@themost/express": "^2.4.0",
|
|
51
|
+
"@themost/json-logger": "^1.3.0",
|
|
52
|
+
"@themost/query": "^2.14.13",
|
|
53
|
+
"@themost/sqlite": "^2.10.1",
|
|
54
|
+
"@themost/xml": "^2.5.2",
|
|
47
55
|
"@types/express": "^4.17.13",
|
|
48
56
|
"@types/http-proxy-middleware": "^1.0.0",
|
|
49
57
|
"@types/jasmine": "^3.10.1",
|
|
58
|
+
"@types/jest": "^29.5.14",
|
|
50
59
|
"@types/node": "^16.11.6",
|
|
51
60
|
"@types/passport": "^1.0.7",
|
|
61
|
+
"@types/passport-http-bearer": "^1.0.41",
|
|
52
62
|
"@types/superagent": "^4.1.14",
|
|
53
63
|
"@types/supertest": "^2.0.11",
|
|
54
64
|
"dotenv": "^10.0.0",
|
|
55
|
-
"
|
|
56
|
-
"jasmine-spec-reporter": "^7.0.0",
|
|
65
|
+
"jest": "^29.7.0",
|
|
57
66
|
"passport-http-bearer": "^1.0.1",
|
|
58
67
|
"rimraf": "^3.0.2",
|
|
59
68
|
"rollup": "^2.59.0",
|
|
60
69
|
"supertest": "^6.1.6",
|
|
61
|
-
"ts-
|
|
70
|
+
"ts-jest": "^29.3.4",
|
|
62
71
|
"tslint": "^6.1.3",
|
|
63
72
|
"typescript": "^4.4.4"
|
|
64
73
|
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { ApplicationBase, ApplicationService, HttpError, TraceUtils } from '@themost/common';
|
|
2
|
+
import { BehaviorSubject } from 'rxjs';
|
|
3
|
+
import { Application } from 'express';
|
|
4
|
+
import { ExpressDataContext } from '@themost/express';
|
|
5
|
+
import request from 'superagent';
|
|
6
|
+
import { URL } from 'url';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { writeFile as writeFileWithCallback, unlink as unlinkWithCallback } from 'fs';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
import crypto from 'crypto';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import { DataCacheStrategy } from '@themost/data';
|
|
13
|
+
import { Certificate, JsonWebKey, VerifySignatureResult, SignJsonSignatureResult, SignerServiceImpl } from './SignerServiceImpl';
|
|
14
|
+
import {Secret, TOTP, TOTPOptions} from 'xotp';
|
|
15
|
+
|
|
16
|
+
const writeFile = promisify(writeFileWithCallback);
|
|
17
|
+
const unlink = promisify(unlinkWithCallback);
|
|
18
|
+
declare interface ApplicationBaseWithContainer extends ApplicationBase {
|
|
19
|
+
container?: BehaviorSubject<Application>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function tmpFile(extension: string = '.tmp'): string {
|
|
23
|
+
const tmpDir = os.tmpdir();
|
|
24
|
+
const filename = `file-${crypto.randomBytes(16).toString('hex')}${extension}`;
|
|
25
|
+
return path.join(tmpDir, filename);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export class SignerService extends ApplicationService implements SignerServiceImpl {
|
|
30
|
+
|
|
31
|
+
protected options?: {
|
|
32
|
+
host?: string;
|
|
33
|
+
user?: string;
|
|
34
|
+
password?: string;
|
|
35
|
+
secret?: string;
|
|
36
|
+
thumbprint?: string;
|
|
37
|
+
inspector?: {
|
|
38
|
+
text?: string[];
|
|
39
|
+
textPosition: string;
|
|
40
|
+
width?: number;
|
|
41
|
+
height?: number;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
constructor(app: ApplicationBase) {
|
|
46
|
+
super(app);
|
|
47
|
+
|
|
48
|
+
// get signer worker configuration
|
|
49
|
+
const options = app.getConfiguration().getSourceAt('settings/universis/signerService') || {};
|
|
50
|
+
|
|
51
|
+
this.options = Object.assign({}, {
|
|
52
|
+
'host': 'http://localhost:2465/'
|
|
53
|
+
}, options);
|
|
54
|
+
|
|
55
|
+
const thisApp = app as ApplicationBaseWithContainer;
|
|
56
|
+
if (thisApp && thisApp.container) {
|
|
57
|
+
thisApp.container.subscribe((container: Application) => {
|
|
58
|
+
if (container) {
|
|
59
|
+
container.get('/api/signer/.well-known/jwks', async (_req: any, res: any, next: any) => {
|
|
60
|
+
try {
|
|
61
|
+
const results = await this.getJwks();
|
|
62
|
+
return res.json(results);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return next(err);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
container.get('/api/signer/.well-known/certs', async (_req: any, res: any, next: any) => {
|
|
68
|
+
try {
|
|
69
|
+
const results = await this.getCerts();
|
|
70
|
+
return res.json(results);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return next(err);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {ExpressDataContext} context The underlying data context
|
|
82
|
+
* @param {*} data The data to be verified
|
|
83
|
+
* @param {{filename: string, contentType: string}=} options Extra attributes (optional)
|
|
84
|
+
* @returns {Promise<VerifySignatureResult[]>}
|
|
85
|
+
*/
|
|
86
|
+
async verify(context: ExpressDataContext, data: unknown, options?: { filename: string; contentType: string }): Promise<VerifySignatureResult[]> {
|
|
87
|
+
if (data == null) {
|
|
88
|
+
throw new Error('The given data cannot be empty during sign.');
|
|
89
|
+
}
|
|
90
|
+
const json = (typeof data === 'object') ? JSON.stringify(data) : JSON.stringify({
|
|
91
|
+
value: data,
|
|
92
|
+
});
|
|
93
|
+
const file = tmpFile('.json');
|
|
94
|
+
// write data to temporary file
|
|
95
|
+
await writeFile(file, json, { encoding: 'utf8' });
|
|
96
|
+
try {
|
|
97
|
+
return await this.verifyFile(context, file, options);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
throw err;
|
|
100
|
+
} finally {
|
|
101
|
+
try {
|
|
102
|
+
await unlink(file); // remove temporary file
|
|
103
|
+
} catch (e) {
|
|
104
|
+
// ignore error
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public async getJwks(): Promise<JsonWebKey[]> {
|
|
110
|
+
try {
|
|
111
|
+
const cache = this.getApplication().getConfiguration().getStrategy(DataCacheStrategy);
|
|
112
|
+
// check if cache is available
|
|
113
|
+
return cache.getOrDefault('/api/signer/keystore/jwks', async () => {
|
|
114
|
+
try {
|
|
115
|
+
const response = await request
|
|
116
|
+
.get(new URL('/keystore/jwks', this.options.host).toString())
|
|
117
|
+
.auth(this.options.user, this.options.password);
|
|
118
|
+
if (response.ok) {
|
|
119
|
+
return response.body;
|
|
120
|
+
}
|
|
121
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
122
|
+
throw new HttpError(response.status, 'Something went wrong while getting json web key set');
|
|
123
|
+
} catch (err) {
|
|
124
|
+
TraceUtils.error(err);
|
|
125
|
+
throw new HttpError(err.status, 'Failed to get json web key set', err.message);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw new HttpError(err.status, 'Failed to get json web key set', err.message);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public async getCerts(): Promise<Certificate[]> {
|
|
134
|
+
const cache = this.getApplication().getConfiguration().getStrategy(DataCacheStrategy);
|
|
135
|
+
return cache.getOrDefault('/api/signer/keystore/certs', async () => {
|
|
136
|
+
try {
|
|
137
|
+
const response = await request.get(new URL('/keystore/certs', this.options.host).toString())
|
|
138
|
+
.auth(this.options.user, this.options.password);
|
|
139
|
+
if (response.ok) {
|
|
140
|
+
return (response.body as Certificate[]).filter((item) => {
|
|
141
|
+
return item.notAfter && new Date(item.notAfter) > new Date();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return [];
|
|
145
|
+
} catch (err) {
|
|
146
|
+
TraceUtils.error(err);
|
|
147
|
+
throw new HttpError(err.status, 'Failed to get certificates', err.message);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Generates a one-time password using the configured secret.
|
|
154
|
+
*/
|
|
155
|
+
public generateVerificationCode(options?: TOTPOptions): string {
|
|
156
|
+
if (this.options.secret == null) {
|
|
157
|
+
throw new Error('One-time password is not supported');
|
|
158
|
+
}
|
|
159
|
+
const secret = Secret.from(this.options.secret, 'base32');
|
|
160
|
+
const opts: TOTPOptions = Object.assign({
|
|
161
|
+
algorithm: 'sha1',
|
|
162
|
+
digits: 6,
|
|
163
|
+
period: 30,
|
|
164
|
+
}, options);
|
|
165
|
+
const totp = new TOTP(opts);
|
|
166
|
+
return totp.generate({ secret });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {ExpressDataContext} context The underlying data context
|
|
171
|
+
* @param {*} data Any JSON object to be signed
|
|
172
|
+
* @param {{timestampServer?:string}} extraOptions
|
|
173
|
+
* @returns {Promise<SignJsonSignatureResult>} An array of signatures
|
|
174
|
+
*/
|
|
175
|
+
async sign(context: ExpressDataContext, data: unknown, extraOptions?: {
|
|
176
|
+
timestampServer?: string,
|
|
177
|
+
reason?: string
|
|
178
|
+
}): Promise<SignJsonSignatureResult> {
|
|
179
|
+
if (data == null) {
|
|
180
|
+
throw new Error('The given data cannot be empty during sign.');
|
|
181
|
+
}
|
|
182
|
+
const json = (typeof data === 'object') ? JSON.stringify(data) : JSON.stringify({
|
|
183
|
+
value: data,
|
|
184
|
+
});
|
|
185
|
+
const file = tmpFile('.json');
|
|
186
|
+
// write data to temporary file
|
|
187
|
+
await writeFile(file, json, { encoding: 'utf8' });
|
|
188
|
+
try {
|
|
189
|
+
return await this.signFinal<SignJsonSignatureResult>(context, file, 'application/json', extraOptions);
|
|
190
|
+
} catch(err) {
|
|
191
|
+
if (err.response) {
|
|
192
|
+
throw new HttpError(err.status, 'Failed to sign data', err.message);
|
|
193
|
+
}
|
|
194
|
+
throw err;
|
|
195
|
+
} finally {
|
|
196
|
+
try {
|
|
197
|
+
await unlink(file); // remove temporary file
|
|
198
|
+
} catch (e) {
|
|
199
|
+
// ignore error
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public async signFile(context: ExpressDataContext, file: string, contentType?: string, extraOptions?: {
|
|
205
|
+
page?: number,
|
|
206
|
+
position?: number[],
|
|
207
|
+
timestampServer?: string,
|
|
208
|
+
reason?: string
|
|
209
|
+
}): Promise<Buffer> {
|
|
210
|
+
return this.signFinal<Buffer>(context, file, contentType, extraOptions);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public async verifyFile(context: ExpressDataContext, file: string, options?: { filename: string; contentType: string }): Promise<VerifySignatureResult[]> {
|
|
214
|
+
try {
|
|
215
|
+
const response = await request.post(new URL('/verify', this.options.host).toString())
|
|
216
|
+
.attach('file',file, options);
|
|
217
|
+
if (response.ok) {
|
|
218
|
+
return response.body;
|
|
219
|
+
}
|
|
220
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
221
|
+
throw new HttpError(response.statusCode);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (err.response) {
|
|
224
|
+
TraceUtils.error(err);
|
|
225
|
+
throw new HttpError(err.response.status, 'Failed to verify data', err.message);
|
|
226
|
+
}
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public async inspect(context: ExpressDataContext, file: string, options?: { filename: string; contentType: string }): Promise<{ page: number, position: number[], text: string }> {
|
|
232
|
+
try {
|
|
233
|
+
const fields: Record<string, string> = {};
|
|
234
|
+
if (this.options.inspector) {
|
|
235
|
+
Object.assign(fields, this.options.inspector);
|
|
236
|
+
}
|
|
237
|
+
const response = await request.post(new URL('/signature/inspect', this.options.host).toString())
|
|
238
|
+
.field(fields)
|
|
239
|
+
.attach('file',file, options);
|
|
240
|
+
if (response.ok) {
|
|
241
|
+
return response.body;
|
|
242
|
+
}
|
|
243
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
244
|
+
throw new HttpError(response.statusCode);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
if (err.response) {
|
|
247
|
+
TraceUtils.error(err);
|
|
248
|
+
throw new HttpError(err.response.status, 'Failed to verify data', err.message);
|
|
249
|
+
}
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public requiresVerificationCode(): boolean {
|
|
255
|
+
return typeof this.options.secret === 'string' && this.options.secret.length > 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
protected async signFinal<T>(context: ExpressDataContext, file: string, contentType?: string, extraOptions?: {
|
|
259
|
+
position?: number[],
|
|
260
|
+
page?: number,
|
|
261
|
+
timestampServer?: string,
|
|
262
|
+
reason?: string
|
|
263
|
+
}): Promise<T> {
|
|
264
|
+
// try to find thumbprint if empty
|
|
265
|
+
if (typeof this.options.thumbprint === 'undefined') {
|
|
266
|
+
const [cert] = await this.getCerts();
|
|
267
|
+
if (cert) {
|
|
268
|
+
this.options.thumbprint = cert.thumbprint;
|
|
269
|
+
} else {
|
|
270
|
+
this.options.thumbprint = '';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const fields = Object.assign({}, extraOptions);
|
|
274
|
+
// if the file is a PDF or the content type is application/pdf, try to inspect signature position
|
|
275
|
+
if (path.extname(file) === '.pdf' || contentType === 'application/pdf' ) {
|
|
276
|
+
if (extraOptions?.page && extraOptions?.position) {
|
|
277
|
+
// if page and position are defined, use them
|
|
278
|
+
Object.assign(fields, {
|
|
279
|
+
page: extraOptions.page,
|
|
280
|
+
position: extraOptions.position.map((element) => {
|
|
281
|
+
return Math.round(element);
|
|
282
|
+
}).join(',')
|
|
283
|
+
});
|
|
284
|
+
} else if (this.options.inspector) {
|
|
285
|
+
// if inspector is defined, use it
|
|
286
|
+
const inspectorResult = await this.inspect(context, file);
|
|
287
|
+
if (inspectorResult?.page && inspectorResult?.position) {
|
|
288
|
+
Object.assign(fields, {
|
|
289
|
+
page: inspectorResult.page,
|
|
290
|
+
position: inspectorResult.position.map((element) => {
|
|
291
|
+
return Math.round(element);
|
|
292
|
+
}).join(',')
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (this.options.thumbprint.length) {
|
|
299
|
+
Object.assign(fields, {
|
|
300
|
+
thumbprint: this.options.thumbprint,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const fileOptions = {
|
|
304
|
+
filename: path.basename(file)
|
|
305
|
+
};
|
|
306
|
+
if (contentType) {
|
|
307
|
+
Object.assign(fileOptions, {
|
|
308
|
+
contentType
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
// if secret is defined, generate one-time password
|
|
313
|
+
if (this.requiresVerificationCode()) {
|
|
314
|
+
Object.assign(fields, {
|
|
315
|
+
otp: this.generateVerificationCode()
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const response = await request.post(new URL('/sign', this.options.host).toString())
|
|
319
|
+
.type('form')
|
|
320
|
+
.auth(this.options.user, this.options.password)
|
|
321
|
+
.field(fields)
|
|
322
|
+
.buffer(contentType !== 'application/json')
|
|
323
|
+
.attach('file',file, fileOptions);
|
|
324
|
+
if (response.ok) {
|
|
325
|
+
return response.body as T;
|
|
326
|
+
}
|
|
327
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
328
|
+
throw new HttpError(response.status);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
if (err.response) {
|
|
331
|
+
TraceUtils.error(err);
|
|
332
|
+
throw new HttpError(err.response.status, 'Failed to sign data', err.message);
|
|
333
|
+
}
|
|
334
|
+
throw err;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ExpressDataContext } from '@themost/express';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export declare interface Certificate {
|
|
5
|
+
version: number;
|
|
6
|
+
subjectDN: string;
|
|
7
|
+
sigAlgName?: string;
|
|
8
|
+
sigAlgOID?: string;
|
|
9
|
+
issuerDN: string;
|
|
10
|
+
serialNumber: any;
|
|
11
|
+
notAfter: string;
|
|
12
|
+
notBefore: string;
|
|
13
|
+
expired: boolean;
|
|
14
|
+
thumbprint: string;
|
|
15
|
+
commonName: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export declare interface JsonWebKey {
|
|
19
|
+
kty: string;
|
|
20
|
+
use: string;
|
|
21
|
+
kid: string;
|
|
22
|
+
alg: string;
|
|
23
|
+
n: string;
|
|
24
|
+
e: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export declare interface VerifySignatureResult {
|
|
28
|
+
valid: boolean;
|
|
29
|
+
certificates: Certificate[];
|
|
30
|
+
signatureProperties?: {
|
|
31
|
+
signingDate: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
signingCertificate: Certificate
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export declare interface SignJsonSignatureResult {
|
|
38
|
+
payload: string;
|
|
39
|
+
signatures: { protected: string; signature: string }[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export declare interface SignerServiceImpl {
|
|
43
|
+
getCerts(): Promise<Certificate[]>;
|
|
44
|
+
getJwks(): Promise<JsonWebKey[]>;
|
|
45
|
+
verify(context: ExpressDataContext, document: string, options?: { filename: string; contentType: string }): Promise<VerifySignatureResult[]>
|
|
46
|
+
verifyFile(context: ExpressDataContext, file: string, options?: { filename: string; contentType: string }): Promise<VerifySignatureResult[]>
|
|
47
|
+
signFile(context: ExpressDataContext, file: string, contentType?: string, extraOptions?: {
|
|
48
|
+
page?: number
|
|
49
|
+
position?: number[],
|
|
50
|
+
timestampServer?: string,
|
|
51
|
+
reason?: string
|
|
52
|
+
}): Promise<Buffer>;
|
|
53
|
+
sign(context: ExpressDataContext, data: unknown, extraOptions?: {
|
|
54
|
+
timestampServer?: string,
|
|
55
|
+
reason?: string
|
|
56
|
+
}): Promise<SignJsonSignatureResult>;
|
|
57
|
+
|
|
58
|
+
}
|
package/src/SignerWorker.ts
CHANGED
|
@@ -1,80 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {ApplicationBase, ApplicationService, HttpError, HttpForbiddenError, TraceUtils} from '@themost/common';
|
|
2
2
|
import { BehaviorSubject } from 'rxjs';
|
|
3
|
-
import { Application, RequestHandler
|
|
3
|
+
import { Application, RequestHandler } from 'express';
|
|
4
4
|
import { signerWorkerRouter } from './signerWorkerRouter';
|
|
5
5
|
import { SignerScopeAccess } from './SignerScopeAccess';
|
|
6
6
|
import { ExpressDataContext, serviceRouter } from '@themost/express';
|
|
7
7
|
import request from 'superagent';
|
|
8
|
-
import
|
|
8
|
+
import { VerifySignatureResult } from './SignerServiceImpl';
|
|
9
|
+
import {URL} from 'url';
|
|
9
10
|
|
|
10
11
|
declare interface ApplicationBaseWithContainer extends ApplicationBase {
|
|
11
12
|
container?: BehaviorSubject<Application>;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
declare interface Certificate {
|
|
15
|
-
version: number;
|
|
16
|
-
subjectDN: string;
|
|
17
|
-
sigAlgName?: string;
|
|
18
|
-
sigAlgOID?: string;
|
|
19
|
-
issuerDN: string;
|
|
20
|
-
serialNumber: any;
|
|
21
|
-
notAfter: string;
|
|
22
|
-
notBefore: string;
|
|
23
|
-
expired: boolean;
|
|
24
|
-
thumbprint: string;
|
|
25
|
-
commonName: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
declare interface VerifySignatureResult {
|
|
30
|
-
valid: boolean;
|
|
31
|
-
certificates: Certificate[];
|
|
32
|
-
signatureProperties?: {
|
|
33
|
-
signingDate: string;
|
|
34
|
-
reason: string;
|
|
35
|
-
signingCertificate: Certificate
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
function insertRouterAfter(parent: Router, after: any, insert: any) {
|
|
41
|
-
const afterIndex = parent.stack.findIndex( (item) => {
|
|
42
|
-
return item === after;
|
|
43
|
-
});
|
|
44
|
-
if (afterIndex < 0) {
|
|
45
|
-
throw new Error('Target router cannot be found in parent stack.');
|
|
46
|
-
}
|
|
47
|
-
const findIndex = parent.stack.findIndex( (item) => {
|
|
48
|
-
return item === insert;
|
|
49
|
-
});
|
|
50
|
-
if (findIndex < 0) {
|
|
51
|
-
throw new Error('Router to be inserted cannot be found in parent stack.');
|
|
52
|
-
}
|
|
53
|
-
// remove last router
|
|
54
|
-
parent.stack.splice(findIndex, 1);
|
|
55
|
-
// move up
|
|
56
|
-
parent.stack.splice(afterIndex + 1, 0, insert);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function insertRouterBefore(parent: Router, before: any, insert: any) {
|
|
60
|
-
const beforeIndex = parent.stack.findIndex( (item) => {
|
|
61
|
-
return item === before;
|
|
62
|
-
});
|
|
63
|
-
if (beforeIndex < 0) {
|
|
64
|
-
throw new Error('Target router cannot be found in parent stack.');
|
|
65
|
-
}
|
|
66
|
-
const findIndex = parent.stack.findIndex( (item) => {
|
|
67
|
-
return item === insert;
|
|
68
|
-
});
|
|
69
|
-
if (findIndex < 0) {
|
|
70
|
-
throw new Error('Router to be inserted cannot be found in parent stack.');
|
|
71
|
-
}
|
|
72
|
-
// remove last router
|
|
73
|
-
parent.stack.splice(findIndex, 1);
|
|
74
|
-
// move up
|
|
75
|
-
parent.stack.splice(beforeIndex, 0, insert);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
15
|
class ScopeAccessConfiguration {
|
|
79
16
|
public elements = [];
|
|
80
17
|
}
|
|
@@ -84,7 +21,7 @@ class ScopeAccessConfiguration {
|
|
|
84
21
|
* @returns {Function}
|
|
85
22
|
*/
|
|
86
23
|
function validateScope(): RequestHandler {
|
|
87
|
-
return (req: any,
|
|
24
|
+
return (req: any, _res: any, next: any) => {
|
|
88
25
|
const scopeAccessConfiguration = req.context.getApplication().getConfiguration().getStrategy(ScopeAccessConfiguration);
|
|
89
26
|
if (scopeAccessConfiguration == null) {
|
|
90
27
|
return next();
|
|
@@ -101,8 +38,17 @@ class ScopeAccessConfiguration {
|
|
|
101
38
|
}
|
|
102
39
|
|
|
103
40
|
export class SignerWorker extends ApplicationService {
|
|
41
|
+
|
|
42
|
+
protected options?: {
|
|
43
|
+
host?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
104
46
|
constructor(app: ApplicationBase) {
|
|
105
47
|
super(app);
|
|
48
|
+
// get configuration
|
|
49
|
+
this.options = Object.assign({
|
|
50
|
+
host: 'http://localhost:2465/'
|
|
51
|
+
}, app.getConfiguration().getSourceAt('settings/universis/signerWorker'));
|
|
106
52
|
const thisApp = app as ApplicationBaseWithContainer;
|
|
107
53
|
if (thisApp && thisApp.container) {
|
|
108
54
|
thisApp.container.subscribe((container: Application) => {
|
|
@@ -113,26 +59,36 @@ export class SignerWorker extends ApplicationService {
|
|
|
113
59
|
scopeAccess.elements.push.apply(scopeAccess.elements, SignerScopeAccess);
|
|
114
60
|
}
|
|
115
61
|
// use signer worker router (for authenticated users)
|
|
116
|
-
serviceRouter.use('/signer/', signerWorkerRouter());
|
|
62
|
+
serviceRouter.use('/signer/', signerWorkerRouter(this.options.host));
|
|
117
63
|
}
|
|
118
64
|
});
|
|
119
65
|
}
|
|
120
66
|
}
|
|
121
67
|
|
|
122
68
|
/**
|
|
123
|
-
*
|
|
69
|
+
*
|
|
124
70
|
* @param {ExpressDataContext} context The underlying data context
|
|
125
71
|
* @param {string} document The file path
|
|
126
72
|
* @param {{filename: string, contentType: string}=} options Extra attributes (optional)
|
|
127
|
-
* @returns
|
|
73
|
+
* @returns Promise<VerifySignatureResult[]>
|
|
128
74
|
*/
|
|
129
75
|
async verify(context: ExpressDataContext, document: string, options?: { filename: string; contentType: string }): Promise<VerifySignatureResult[]> {
|
|
130
|
-
|
|
131
|
-
.
|
|
132
|
-
|
|
133
|
-
|
|
76
|
+
try {
|
|
77
|
+
const response = await request.post(new URL('/verify', this.options.host).toString())
|
|
78
|
+
.attach('file',document, options);
|
|
79
|
+
if (response.ok) {
|
|
80
|
+
return response.body;
|
|
81
|
+
}
|
|
82
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
83
|
+
throw new HttpError(response.statusCode);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err.response) {
|
|
86
|
+
TraceUtils.error(err);
|
|
87
|
+
throw new HttpError(err.response.status);
|
|
88
|
+
}
|
|
89
|
+
throw err;
|
|
134
90
|
}
|
|
135
|
-
|
|
91
|
+
|
|
136
92
|
}
|
|
137
93
|
|
|
138
94
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,16 +2,17 @@ import { HttpMethodNotAllowedError } from '@themost/common';
|
|
|
2
2
|
import express from 'express';
|
|
3
3
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
4
4
|
|
|
5
|
-
export function signerWorkerRouter() {
|
|
5
|
+
export function signerWorkerRouter(target: string = 'http://localhost:2465/') {
|
|
6
6
|
const router = express.Router();
|
|
7
7
|
router.use('/sign', (req, res, next) => {
|
|
8
8
|
return next(new HttpMethodNotAllowedError('This method is not supported when using remote services.'))
|
|
9
9
|
});
|
|
10
10
|
router.use(createProxyMiddleware({
|
|
11
|
-
target
|
|
11
|
+
target,
|
|
12
12
|
pathRewrite: {
|
|
13
13
|
'^/api/signer' : ''
|
|
14
14
|
},
|
|
15
|
+
logLevel: 'warn',
|
|
15
16
|
changeOrigin: true
|
|
16
17
|
}));
|
|
17
18
|
return router;
|