@universis/signer-worker 1.1.0 → 1.6.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 +122 -3
- package/dist/RemoteSignerWorker.d.ts +7 -0
- package/dist/SignerService.d.ts +72 -0
- package/dist/SignerServiceImpl.d.ts +61 -0
- package/dist/SignerWorker.d.ts +10 -23
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +380 -13
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +384 -11
- package/dist/index.js.map +1 -1
- package/dist/remoteSignerWorkerRouter.d.ts +1 -0
- package/dist/signerWorkerRouter.d.ts +1 -1
- package/jest.setup.js +23 -0
- package/package.json +16 -7
- package/src/RemoteSignerWorker.ts +34 -0
- package/src/SignerService.ts +339 -0
- package/src/SignerServiceImpl.ts +58 -0
- package/src/SignerWorker.ts +39 -79
- package/src/index.ts +5 -1
- package/src/remoteSignerWorkerRouter.ts +16 -0
- package/src/signerWorkerRouter.ts +3 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ApplicationBase } from '@themost/common';
|
|
2
|
+
import { SignerWorker, ApplicationBaseWithContainer } from './SignerWorker';
|
|
3
|
+
import { Application } from 'express';
|
|
4
|
+
import { remoteSignerWorkerRouter } from './remoteSignerWorkerRouter';
|
|
5
|
+
|
|
6
|
+
class ScopeAccessConfiguration {
|
|
7
|
+
public elements = [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class RemoteSignerWorker extends SignerWorker {
|
|
11
|
+
|
|
12
|
+
constructor(app: ApplicationBase) {
|
|
13
|
+
super(app);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
install(): void {
|
|
17
|
+
// get configuration
|
|
18
|
+
this.options = Object.assign({
|
|
19
|
+
host: 'http://localhost:8480/'
|
|
20
|
+
}, this.getApplication().getConfiguration().getSourceAt('settings/universis/remoteSignerWorker'));
|
|
21
|
+
const thisApp = this.getApplication() as ApplicationBaseWithContainer;
|
|
22
|
+
if (thisApp && thisApp.container) {
|
|
23
|
+
thisApp.container.subscribe((container: Application) => {
|
|
24
|
+
if (container) {
|
|
25
|
+
// use signer worker router (for authenticated users)
|
|
26
|
+
container.use('/services/remoteSigner/', remoteSignerWorkerRouter(this.options.host));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { RemoteSignerWorker };
|
|
@@ -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
|
-
declare interface ApplicationBaseWithContainer extends ApplicationBase {
|
|
11
|
+
export 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,38 +38,61 @@ 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);
|
|
106
|
-
|
|
48
|
+
this.install();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
install(): void {
|
|
52
|
+
// get configuration
|
|
53
|
+
this.options = Object.assign({
|
|
54
|
+
host: 'http://localhost:2465/'
|
|
55
|
+
}, this.getApplication().getConfiguration().getSourceAt('settings/universis/signerWorker'));
|
|
56
|
+
const thisApp = this.getApplication() as ApplicationBaseWithContainer;
|
|
107
57
|
if (thisApp && thisApp.container) {
|
|
108
58
|
thisApp.container.subscribe((container: Application) => {
|
|
109
59
|
if (container) {
|
|
110
60
|
// add scope access
|
|
111
|
-
const scopeAccess =
|
|
61
|
+
const scopeAccess = thisApp.getConfiguration().getStrategy(ScopeAccessConfiguration);
|
|
112
62
|
if (scopeAccess) {
|
|
113
63
|
scopeAccess.elements.push.apply(scopeAccess.elements, SignerScopeAccess);
|
|
114
64
|
}
|
|
115
65
|
// use signer worker router (for authenticated users)
|
|
116
|
-
serviceRouter.use('/signer/', signerWorkerRouter());
|
|
66
|
+
serviceRouter.use('/signer/', signerWorkerRouter(this.options.host));
|
|
117
67
|
}
|
|
118
68
|
});
|
|
119
69
|
}
|
|
120
70
|
}
|
|
121
71
|
|
|
122
72
|
/**
|
|
123
|
-
*
|
|
73
|
+
*
|
|
124
74
|
* @param {ExpressDataContext} context The underlying data context
|
|
125
75
|
* @param {string} document The file path
|
|
126
76
|
* @param {{filename: string, contentType: string}=} options Extra attributes (optional)
|
|
127
|
-
* @returns
|
|
77
|
+
* @returns Promise<VerifySignatureResult[]>
|
|
128
78
|
*/
|
|
129
79
|
async verify(context: ExpressDataContext, document: string, options?: { filename: string; contentType: string }): Promise<VerifySignatureResult[]> {
|
|
130
|
-
|
|
131
|
-
.
|
|
132
|
-
|
|
133
|
-
|
|
80
|
+
try {
|
|
81
|
+
const response = await request.post(new URL('/verify', this.options.host).toString())
|
|
82
|
+
.attach('file',document, options);
|
|
83
|
+
if (response.ok) {
|
|
84
|
+
return response.body;
|
|
85
|
+
}
|
|
86
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
87
|
+
throw new HttpError(response.statusCode);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err.response) {
|
|
90
|
+
TraceUtils.error(err);
|
|
91
|
+
throw new HttpError(err.response.status);
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
134
94
|
}
|
|
135
|
-
|
|
95
|
+
|
|
136
96
|
}
|
|
137
97
|
|
|
138
98
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export * from './SignerWorker';
|
|
2
|
-
export * from './
|
|
2
|
+
export * from './SignerServiceImpl';
|
|
3
|
+
export * from './signerWorkerRouter';
|
|
4
|
+
export * from './SignerService';
|
|
5
|
+
export * from './RemoteSignerWorker';
|
|
6
|
+
export * from './remoteSignerWorkerRouter';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { HttpMethodNotAllowedError } from '@themost/common';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
4
|
+
|
|
5
|
+
export function remoteSignerWorkerRouter(target: string = 'http://localhost:8480/') {
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
router.use(createProxyMiddleware({
|
|
8
|
+
target,
|
|
9
|
+
pathRewrite: {
|
|
10
|
+
'^/services/remoteSigner' : ''
|
|
11
|
+
},
|
|
12
|
+
logLevel: 'warn',
|
|
13
|
+
changeOrigin: true
|
|
14
|
+
}));
|
|
15
|
+
return router;
|
|
16
|
+
}
|
|
@@ -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;
|