@flancer32/teq-web 0.1.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/.github/workflows/npm-publish.yml +48 -0
- package/LICENSE +201 -0
- package/README.md +125 -0
- package/eslint.config.js +37 -0
- package/package.json +46 -0
- package/src/Back/Api/Handler.js +26 -0
- package/src/Back/Defaults.js +6 -0
- package/src/Back/Dispatcher.js +115 -0
- package/src/Back/Dto/Handler/Info.js +68 -0
- package/src/Back/Enum/Server/Type.js +12 -0
- package/src/Back/Enum/Stage.js +10 -0
- package/src/Back/Handler/Pre/Log.js +45 -0
- package/src/Back/Handler/Static.js +139 -0
- package/src/Back/Helper/Cast.js +86 -0
- package/src/Back/Helper/Mime.js +90 -0
- package/src/Back/Helper/Order/Kahn.js +66 -0
- package/src/Back/Helper/Respond.js +78 -0
- package/src/Back/Logger.js +53 -0
- package/src/Back/Server/Config/Tls.js +55 -0
- package/src/Back/Server/Config.js +69 -0
- package/src/Back/Server.js +92 -0
- package/test/accept/Server.test.mjs +105 -0
- package/test/certs/ca.pem +19 -0
- package/test/certs/cert.pem +19 -0
- package/test/certs/key.pem +28 -0
- package/test/dev/app/Plugin/Start.js +38 -0
- package/test/dev/app.mjs +65 -0
- package/test/dev/web/favicon.ico +0 -0
- package/test/dev/web/index.html +11 -0
- package/test/unit/Back/Helper/Order/Kahn.test.mjs +59 -0
- package/test/unit/Back/Server.test.mjs +112 -0
- package/test/unit/common.js +22 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for server configuration DTO.
|
|
3
|
+
* Supports HTTP, HTTPS and HTTP2 server types with TLS configuration.
|
|
4
|
+
*/
|
|
5
|
+
export default class Fl32_Web_Back_Server_Config {
|
|
6
|
+
/* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
|
|
7
|
+
/**
|
|
8
|
+
* @param {Fl32_Web_Back_Helper_Cast} cast
|
|
9
|
+
* @param {typeof Fl32_Web_Back_Enum_Server_Type} SERVER_TYPE
|
|
10
|
+
* @param {Fl32_Web_Back_Server_Config_Tls} tlsFactory
|
|
11
|
+
*/
|
|
12
|
+
constructor(
|
|
13
|
+
{
|
|
14
|
+
Fl32_Web_Back_Helper_Cast$: cast,
|
|
15
|
+
Fl32_Web_Back_Enum_Server_Type$: SERVER_TYPE,
|
|
16
|
+
Fl32_Web_Back_Server_Config_Tls$: tlsFactory,
|
|
17
|
+
}
|
|
18
|
+
) {
|
|
19
|
+
/* eslint-enable jsdoc/require-param-description,jsdoc/check-param-names */
|
|
20
|
+
// INSTANCE METHODS
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new DTO instance with properly casted attributes.
|
|
23
|
+
* Ensures valid values for enums and numerical fields.
|
|
24
|
+
* Validates TLS configuration when type is HTTPS.
|
|
25
|
+
*
|
|
26
|
+
* @param {Fl32_Web_Back_Server_Config.Dto|object} [data] - Raw input data for the DTO.
|
|
27
|
+
* @returns {Dto} - A properly structured DTO instance.
|
|
28
|
+
* @throws {Error} When HTTPS type is specified without TLS configuration.
|
|
29
|
+
*/
|
|
30
|
+
this.create = function (data) {
|
|
31
|
+
const res = Object.assign(new Dto(), data);
|
|
32
|
+
if (data) {
|
|
33
|
+
res.port = cast.int(data.port);
|
|
34
|
+
res.type = cast.enum(data.type, SERVER_TYPE, {lower: true});
|
|
35
|
+
|
|
36
|
+
if (data.tls) {
|
|
37
|
+
res.tls = tlsFactory.create(data.tls);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (res.type === SERVER_TYPE.HTTPS && !res.tls) {
|
|
41
|
+
throw new Error('TLS configuration is required for HTTPS server type');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return res;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @memberOf Fl32_Web_Back_Server_Config
|
|
51
|
+
*/
|
|
52
|
+
class Dto {
|
|
53
|
+
/**
|
|
54
|
+
* Port to listening (3000).
|
|
55
|
+
*
|
|
56
|
+
* @type {number}
|
|
57
|
+
*/
|
|
58
|
+
port;
|
|
59
|
+
/**
|
|
60
|
+
* @type {string}
|
|
61
|
+
* @see Fl32_Web_Back_Enum_Server_Type
|
|
62
|
+
*/
|
|
63
|
+
type;
|
|
64
|
+
/**
|
|
65
|
+
* TLS configuration for HTTPS server.
|
|
66
|
+
* @type {Fl32_Web_Back_Server_Config_Tls.Dto|undefined}
|
|
67
|
+
*/
|
|
68
|
+
tls;
|
|
69
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web server implementation supporting HTTP/1 and HTTP/2 protocols.
|
|
3
|
+
* Handles incoming requests and delegates them to the dispatcher.
|
|
4
|
+
*
|
|
5
|
+
* @property {function(): module:http.Server} getInstance - Returns the server instance.
|
|
6
|
+
* @property {function(Fl32_Web_Back_Server_Config.Dto): Promise<void>} start - Starts the server with given configuration.
|
|
7
|
+
* @property {function(): Promise<void>} stop - Stops the server.
|
|
8
|
+
*/
|
|
9
|
+
export default class Fl32_Web_Back_Server {
|
|
10
|
+
/* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
|
|
11
|
+
/**
|
|
12
|
+
* @param {typeof import('node:http')} http
|
|
13
|
+
* @param {typeof import('node:http2')} http2
|
|
14
|
+
* @param {Fl32_Web_Back_Defaults} DEF
|
|
15
|
+
* @param {Fl32_Web_Back_Logger} logger
|
|
16
|
+
* @param {Fl32_Web_Back_Dispatcher} dispatcher
|
|
17
|
+
* @param {typeof Fl32_Web_Back_Enum_Server_Type} SERVER_TYPE
|
|
18
|
+
*/
|
|
19
|
+
constructor(
|
|
20
|
+
{
|
|
21
|
+
'node:http': http,
|
|
22
|
+
'node:http2': http2,
|
|
23
|
+
Fl32_Web_Back_Defaults$: DEF,
|
|
24
|
+
Fl32_Web_Back_Logger$: logger,
|
|
25
|
+
Fl32_Web_Back_Dispatcher$: dispatcher,
|
|
26
|
+
Fl32_Web_Back_Enum_Server_Type$: SERVER_TYPE,
|
|
27
|
+
}
|
|
28
|
+
) {
|
|
29
|
+
/* eslint-enable jsdoc/require-param-description,jsdoc/check-param-names */
|
|
30
|
+
// VARS
|
|
31
|
+
const {createServer} = http;
|
|
32
|
+
const {createServer: createServerH2, createSecureServer} = http2;
|
|
33
|
+
/** @type {module:http.Server} */
|
|
34
|
+
let _instance;
|
|
35
|
+
|
|
36
|
+
// MAIN
|
|
37
|
+
/**
|
|
38
|
+
* @returns {module:http.Server}
|
|
39
|
+
*/
|
|
40
|
+
this.getInstance = () => _instance;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Starts the server with optional configuration.
|
|
44
|
+
* @param {Fl32_Web_Back_Server_Config.Dto} [cfg] - Server configuration
|
|
45
|
+
* @returns {Promise<void>}
|
|
46
|
+
*/
|
|
47
|
+
this.start = async function (cfg) {
|
|
48
|
+
// order handlers in the dispatcher
|
|
49
|
+
dispatcher.orderHandlers();
|
|
50
|
+
// create server
|
|
51
|
+
const port = cfg?.port ?? DEF.PORT;
|
|
52
|
+
const type = cfg?.type ?? SERVER_TYPE.HTTP;
|
|
53
|
+
|
|
54
|
+
if (type === SERVER_TYPE.HTTP2) {
|
|
55
|
+
_instance = createServerH2();
|
|
56
|
+
logger.info(`Starting server in HTTP/2 mode on port ${port}...`);
|
|
57
|
+
} else if (type === SERVER_TYPE.HTTP) {
|
|
58
|
+
_instance = createServer({});
|
|
59
|
+
logger.info(`Starting server in HTTP/1 mode on port ${port}...`);
|
|
60
|
+
} else if (type === SERVER_TYPE.HTTPS) {
|
|
61
|
+
if (!cfg.tls?.key || !cfg.tls?.cert) {
|
|
62
|
+
logger.error('HTTPS server requires TLS key and certificate');
|
|
63
|
+
throw new Error('TLS key and certificate are required for HTTPS server');
|
|
64
|
+
}
|
|
65
|
+
_instance = createSecureServer(cfg.tls);
|
|
66
|
+
logger.info(`Starting server in HTTPS (HTTP/2 + TLS) mode on port ${port}...`);
|
|
67
|
+
} else {
|
|
68
|
+
logger.error(`Unsupported server type: ${type}`);
|
|
69
|
+
throw new Error(`Server type '${type}' is not supported`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_instance.on('request', dispatcher.onEventRequest);
|
|
73
|
+
_instance.listen(port);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Stops the server.
|
|
78
|
+
* @returns {Promise<void>}
|
|
79
|
+
*/
|
|
80
|
+
this.stop = async function () {
|
|
81
|
+
if (_instance) {
|
|
82
|
+
await new Promise((resolve, reject) => {
|
|
83
|
+
_instance.close(err => err ? reject(err) : resolve());
|
|
84
|
+
});
|
|
85
|
+
logger.info('Server stopped');
|
|
86
|
+
_instance = undefined;
|
|
87
|
+
} else {
|
|
88
|
+
logger.warn('Server is not running');
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {describe, it} from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {readFileSync} from 'node:fs';
|
|
4
|
+
import {join, dirname} from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {buildTestContainer} from '../unit/common.js';
|
|
7
|
+
|
|
8
|
+
async function waitListening(server) {
|
|
9
|
+
if (!server.getInstance().listening) {
|
|
10
|
+
await new Promise(res => server.getInstance().once('listening', res));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('Fl32_Web_Back_Server (real)', () => {
|
|
15
|
+
it('should start and respond in HTTP/1 mode', async () => {
|
|
16
|
+
const container = buildTestContainer();
|
|
17
|
+
const server = await container.get('Fl32_Web_Back_Server$');
|
|
18
|
+
const SERVER_TYPE = await container.get('Fl32_Web_Back_Enum_Server_Type$');
|
|
19
|
+
const Config = await container.get('Fl32_Web_Back_Server_Config$');
|
|
20
|
+
const http = await container.get('node:http');
|
|
21
|
+
|
|
22
|
+
const cfg = Config.create({ port: 3051, type: SERVER_TYPE.HTTP });
|
|
23
|
+
await server.start(cfg);
|
|
24
|
+
await waitListening(server);
|
|
25
|
+
|
|
26
|
+
const status = await new Promise((resolve, reject) => {
|
|
27
|
+
http.get(`http://localhost:${cfg.port}`, res => {
|
|
28
|
+
const {statusCode} = res;
|
|
29
|
+
res.resume();
|
|
30
|
+
res.on('end', () => resolve(statusCode));
|
|
31
|
+
}).on('error', reject);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.strictEqual(status, 404);
|
|
35
|
+
await server.stop();
|
|
36
|
+
assert.strictEqual(server.getInstance(), undefined);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should start and respond in HTTP/2 mode', async () => {
|
|
40
|
+
const container = buildTestContainer();
|
|
41
|
+
const server = await container.get('Fl32_Web_Back_Server$');
|
|
42
|
+
const SERVER_TYPE = await container.get('Fl32_Web_Back_Enum_Server_Type$');
|
|
43
|
+
const Config = await container.get('Fl32_Web_Back_Server_Config$');
|
|
44
|
+
const http2 = await container.get('node:http2');
|
|
45
|
+
|
|
46
|
+
const cfg = Config.create({ port: 3052, type: SERVER_TYPE.HTTP2 });
|
|
47
|
+
await server.start(cfg);
|
|
48
|
+
await waitListening(server);
|
|
49
|
+
|
|
50
|
+
const status = await new Promise((resolve, reject) => {
|
|
51
|
+
const client = http2.connect(`http://localhost:${cfg.port}`);
|
|
52
|
+
client.on('error', reject);
|
|
53
|
+
const req = client.request({ ':path': '/' });
|
|
54
|
+
req.on('response', headers => {
|
|
55
|
+
resolve(headers[':status']);
|
|
56
|
+
});
|
|
57
|
+
req.on('end', () => client.close());
|
|
58
|
+
req.end();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
assert.strictEqual(status, 404);
|
|
62
|
+
await server.stop();
|
|
63
|
+
assert.strictEqual(server.getInstance(), undefined);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should start and respond in HTTPS mode', async () => {
|
|
67
|
+
const container = buildTestContainer();
|
|
68
|
+
const server = await container.get('Fl32_Web_Back_Server$');
|
|
69
|
+
const SERVER_TYPE = await container.get('Fl32_Web_Back_Enum_Server_Type$');
|
|
70
|
+
const Config = await container.get('Fl32_Web_Back_Server_Config$');
|
|
71
|
+
const http2 = await container.get('node:http2');
|
|
72
|
+
|
|
73
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
74
|
+
const certDir = join(dir, '..', 'certs');
|
|
75
|
+
const key = readFileSync(join(certDir, 'key.pem'), 'utf8');
|
|
76
|
+
const cert = readFileSync(join(certDir, 'cert.pem'), 'utf8');
|
|
77
|
+
let ca;
|
|
78
|
+
try { ca = readFileSync(join(certDir, 'ca.pem'), 'utf8'); } catch {}
|
|
79
|
+
|
|
80
|
+
const cfg = Config.create({
|
|
81
|
+
port: 3053,
|
|
82
|
+
type: SERVER_TYPE.HTTPS,
|
|
83
|
+
tls: {key, cert, ca}
|
|
84
|
+
});
|
|
85
|
+
await server.start(cfg);
|
|
86
|
+
await waitListening(server);
|
|
87
|
+
|
|
88
|
+
const status = await new Promise((resolve, reject) => {
|
|
89
|
+
const client = http2.connect(`https://localhost:${cfg.port}`, {
|
|
90
|
+
rejectUnauthorized: false,
|
|
91
|
+
});
|
|
92
|
+
client.on('error', reject);
|
|
93
|
+
const req = client.request({ ':path': '/' });
|
|
94
|
+
req.on('response', headers => {
|
|
95
|
+
resolve(headers[':status']);
|
|
96
|
+
});
|
|
97
|
+
req.on('end', () => client.close());
|
|
98
|
+
req.end();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
assert.strictEqual(status, 404);
|
|
102
|
+
await server.stop();
|
|
103
|
+
assert.strictEqual(server.getInstance(), undefined);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDCTCCAfGgAwIBAgIUBpZ2v9fA8XfqVF83Nw5byUa0ylswDQYJKoZIhvcNAQEL
|
|
3
|
+
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDYwODA5MDA1N1oXDTI2MDYw
|
|
4
|
+
ODA5MDA1N1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
|
5
|
+
AAOCAQ8AMIIBCgKCAQEAymikxJnbPFs3H/rRzetPjUdOPVuMOTfkz5oBDcP7s/2W
|
|
6
|
+
O7135UPbDGzClYyYJvwAZm9i1Vivzm6KrNDZR0lawqYVf6COGJSAtMrkzx+bFSh7
|
|
7
|
+
HWhuk4ByAdoZr6kFzpYBrxcfjSLU2oqaCyzb5vfksfrk1Yyr/i8DK6sw640OUlzP
|
|
8
|
+
1rtKTuRCpYm8We/wrxEvrcdzk7FVFYLCzQfGOZLPmZpoDI3pdUJ/wkY4tjPAPFLn
|
|
9
|
+
143L+07ZLN7Zuf0mpRAGpKve+MWzfId4xXoJ2XpGolLEkhJhfDNI9DCBjEZ3cYmJ
|
|
10
|
+
LYSkpevQuTJNQm/89ivPk3FV0p3H4KjKKgtvtUaOZQIDAQABo1MwUTAdBgNVHQ4E
|
|
11
|
+
FgQUjriVSWVYIJ/a21utkNLU2Cvjl1swHwYDVR0jBBgwFoAUjriVSWVYIJ/a21ut
|
|
12
|
+
kNLU2Cvjl1swDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAkcHr
|
|
13
|
+
TPlqzehyG1KGGb7VAGwCoqhUJfz0Pd3z2p+UOoKq/EmC8b1FRgKDZxSQHk0hhQWE
|
|
14
|
+
M4f51tTbv2LgskfOF/S/tDoKBGnOwYS0oFmVqEstHuC2uto8ukj3FNvsZW58x4Q1
|
|
15
|
+
DOGLkUqlPj5vAh8Zhw8GWokorF11dZlnKdPTC3NCdqyPTWNQPeSqd2qGsFgMBB2i
|
|
16
|
+
q49QsUCNqiVc5ZfpbTBq+Jk1qHd1Pyf9Dlxm3Wi6RBguBcpHooZTAAYleSk+uhSa
|
|
17
|
+
xH9f8v9E2aeYAk/zsZQt2f3x9sZQCAfjHPDOzXydiv568FasLxACY12/GkMNGdvy
|
|
18
|
+
xlgkz6c0z3TS8kFxrQ==
|
|
19
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDCTCCAfGgAwIBAgIUBpZ2v9fA8XfqVF83Nw5byUa0ylswDQYJKoZIhvcNAQEL
|
|
3
|
+
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDYwODA5MDA1N1oXDTI2MDYw
|
|
4
|
+
ODA5MDA1N1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
|
5
|
+
AAOCAQ8AMIIBCgKCAQEAymikxJnbPFs3H/rRzetPjUdOPVuMOTfkz5oBDcP7s/2W
|
|
6
|
+
O7135UPbDGzClYyYJvwAZm9i1Vivzm6KrNDZR0lawqYVf6COGJSAtMrkzx+bFSh7
|
|
7
|
+
HWhuk4ByAdoZr6kFzpYBrxcfjSLU2oqaCyzb5vfksfrk1Yyr/i8DK6sw640OUlzP
|
|
8
|
+
1rtKTuRCpYm8We/wrxEvrcdzk7FVFYLCzQfGOZLPmZpoDI3pdUJ/wkY4tjPAPFLn
|
|
9
|
+
143L+07ZLN7Zuf0mpRAGpKve+MWzfId4xXoJ2XpGolLEkhJhfDNI9DCBjEZ3cYmJ
|
|
10
|
+
LYSkpevQuTJNQm/89ivPk3FV0p3H4KjKKgtvtUaOZQIDAQABo1MwUTAdBgNVHQ4E
|
|
11
|
+
FgQUjriVSWVYIJ/a21utkNLU2Cvjl1swHwYDVR0jBBgwFoAUjriVSWVYIJ/a21ut
|
|
12
|
+
kNLU2Cvjl1swDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAkcHr
|
|
13
|
+
TPlqzehyG1KGGb7VAGwCoqhUJfz0Pd3z2p+UOoKq/EmC8b1FRgKDZxSQHk0hhQWE
|
|
14
|
+
M4f51tTbv2LgskfOF/S/tDoKBGnOwYS0oFmVqEstHuC2uto8ukj3FNvsZW58x4Q1
|
|
15
|
+
DOGLkUqlPj5vAh8Zhw8GWokorF11dZlnKdPTC3NCdqyPTWNQPeSqd2qGsFgMBB2i
|
|
16
|
+
q49QsUCNqiVc5ZfpbTBq+Jk1qHd1Pyf9Dlxm3Wi6RBguBcpHooZTAAYleSk+uhSa
|
|
17
|
+
xH9f8v9E2aeYAk/zsZQt2f3x9sZQCAfjHPDOzXydiv568FasLxACY12/GkMNGdvy
|
|
18
|
+
xlgkz6c0z3TS8kFxrQ==
|
|
19
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-----BEGIN PRIVATE KEY-----
|
|
2
|
+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDKaKTEmds8Wzcf
|
|
3
|
+
+tHN60+NR049W4w5N+TPmgENw/uz/ZY7vXflQ9sMbMKVjJgm/ABmb2LVWK/Oboqs
|
|
4
|
+
0NlHSVrCphV/oI4YlIC0yuTPH5sVKHsdaG6TgHIB2hmvqQXOlgGvFx+NItTaipoL
|
|
5
|
+
LNvm9+Sx+uTVjKv+LwMrqzDrjQ5SXM/Wu0pO5EKlibxZ7/CvES+tx3OTsVUVgsLN
|
|
6
|
+
B8Y5ks+ZmmgMjel1Qn/CRji2M8A8UufXjcv7Ttks3tm5/SalEAakq974xbN8h3jF
|
|
7
|
+
egnZekaiUsSSEmF8M0j0MIGMRndxiYkthKSl69C5Mk1Cb/z2K8+TcVXSncfgqMoq
|
|
8
|
+
C2+1Ro5lAgMBAAECggEAEva0lUI6/aUoKC62Ox4++6QWIFPe8o9vjcRxMSp5qcvq
|
|
9
|
+
Uu+LTPzLXZclBfZAXSACzkDFAusbvEdeu8FCJ4EnvTvqoLoDW38yWH3367CbiuMa
|
|
10
|
+
NyT30zRXZMqxLw7ddJUDqbViEeA/uWKp+xO5Xfg/bNjDrtROsEe++szqY5n5dl2B
|
|
11
|
+
aJLPEtg2OQW7GWKF7PX9YyZmbZds+L7WFhTE+42zfO6MMQm5jaTr8er74u6FnEA2
|
|
12
|
+
ewrJmB7i9CS1DfYhCnJCwqOIxr9rPp9WMvRNfmL8wwqAr+4f1TidI7aKpA3vt2d0
|
|
13
|
+
HOrnNws+08tlcqQYHHyasLzBzsYGAZRTvCoCxjVd8wKBgQDkWjNxp9TmNxQXxvhB
|
|
14
|
+
J42lmo+9xBYzKUzmMMSvS7RoPFG8myf1s82qPTiwZZzGlpR6XIXfSZKQ45YhYz7S
|
|
15
|
+
8i8VbhjVtGyOmExBRYryI3y8pC1aVHk6bPQl8rATIkqsIGN8VHSiwCMeV+j6VWUP
|
|
16
|
+
ZYjb0iqS2zKhhXD9CVcXIT/SfwKBgQDi6lG64nI/ECjakXfrXa+J1T5NJE+OEP3J
|
|
17
|
+
AR9nqRiR/VcwAFMBiJwFfm07dv4WHLbfI1qdx+OU4aSsBohyLg3X9WO0Y8iGw2w4
|
|
18
|
+
V6GGiwGK009OfzdrwTM9QjVHhk23SlFeQFPib4uRGcRnjimMNY6M1E3qvXHpyVXV
|
|
19
|
+
rpvxyGElGwKBgGjcXwlPJ738BvcQQIoy7qHggyeCdytRSOXf+UICQrsnD+XLXiM/
|
|
20
|
+
SS9m47RlRQQQu+ggur0ZnPt590Qnvf7ChgqSP0dLjhpBJ6tFkxO0ZiB+R/FWH0FM
|
|
21
|
+
LSWL930h3yaBzQ2X/uOJ1damSe9C7aCPYLSJI1HC5NI1Y/hepKaTdypjAoGAEfkr
|
|
22
|
+
ZhkfoX0fL0jMbdkq2UkJuUSCBKe14mDzYtuS9aVSbZvo9zsh2JGOB2LCd2/o0D3V
|
|
23
|
+
pJ+7mARTbcjKr/iT4iIutpAcxwfdn4zZX3XNNnjMVFRhSGiyLUz8OWEa8MSzMzr3
|
|
24
|
+
Kf1Z2bFnzCgHhHKNivwZ+9jrl+/5m4ZMFdegUjcCgYEAu/cKxIBc3Lb4gVZx6nHp
|
|
25
|
+
bPr9flDbdCKQk20kpVPvNxq67FuVyKy3n0svRX+fZ9uPhuv20IWkTBue3yCGKpjD
|
|
26
|
+
kUJ2A9Y9FZGSwhU3LSo6grdEv3pIW1YLTl95nUO20bRWCWcwsoV5/GEhFkEZWq06
|
|
27
|
+
wZCQXFhIY78gzd4gk+qJQpg=
|
|
28
|
+
-----END PRIVATE KEY-----
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export default class App_Plugin_Start {
|
|
2
|
+
/**
|
|
3
|
+
* @param {typeof import('node:path')} path
|
|
4
|
+
* @param {typeof import('node:url')} url
|
|
5
|
+
* @param {Fl32_Web_Back_Dispatcher} dispatcher
|
|
6
|
+
* @param {Fl32_Web_Back_Handler_Pre_Log} hndlRequestLog
|
|
7
|
+
* @param {Fl32_Web_Back_Handler_Static} hndlStatic
|
|
8
|
+
*/
|
|
9
|
+
constructor(
|
|
10
|
+
{
|
|
11
|
+
'node:path': path,
|
|
12
|
+
'node:url': url,
|
|
13
|
+
Fl32_Web_Back_Dispatcher$: dispatcher,
|
|
14
|
+
Fl32_Web_Back_Handler_Pre_Log$: hndlRequestLog,
|
|
15
|
+
Fl32_Web_Back_Handler_Static$: hndlStatic,
|
|
16
|
+
}
|
|
17
|
+
) {
|
|
18
|
+
// VARS
|
|
19
|
+
const {dirname, join} = path;
|
|
20
|
+
const {fileURLToPath} = url;
|
|
21
|
+
|
|
22
|
+
// MAIN
|
|
23
|
+
/* Resolve a path to the root folder. */
|
|
24
|
+
const metaUrl = new URL(import.meta.url);
|
|
25
|
+
const script = fileURLToPath(metaUrl);
|
|
26
|
+
const cur = dirname(script);
|
|
27
|
+
const root = join(cur, '..', '..');
|
|
28
|
+
const webRoot = join(root, 'web');
|
|
29
|
+
|
|
30
|
+
return async function () {
|
|
31
|
+
// Set up handlers
|
|
32
|
+
await hndlStatic.init({rootPath: webRoot});
|
|
33
|
+
// Register handlers
|
|
34
|
+
dispatcher.addHandler(hndlRequestLog);
|
|
35
|
+
dispatcher.addHandler(hndlStatic);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
package/test/dev/app.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* A test script to emulate an app that uses the web server.
|
|
5
|
+
*/
|
|
6
|
+
import {dirname, join} from 'node:path';
|
|
7
|
+
import {fileURLToPath} from 'node:url';
|
|
8
|
+
import {readFileSync} from 'node:fs';
|
|
9
|
+
import Container from '@teqfw/di';
|
|
10
|
+
|
|
11
|
+
// VARS
|
|
12
|
+
/* Resolve a path to the root folder. */
|
|
13
|
+
const url = new URL(import.meta.url);
|
|
14
|
+
const script = fileURLToPath(url);
|
|
15
|
+
const cur = dirname(script);
|
|
16
|
+
const root = join(cur, '..', '..');
|
|
17
|
+
|
|
18
|
+
// Create a new instance of the container
|
|
19
|
+
const container = new Container();
|
|
20
|
+
|
|
21
|
+
// Get the resolver from the container
|
|
22
|
+
const resolver = container.getResolver();
|
|
23
|
+
resolver.addNamespaceRoot('Fl32_Web_', join(root, 'src'));
|
|
24
|
+
resolver.addNamespaceRoot('App_', join(cur, 'app'));
|
|
25
|
+
|
|
26
|
+
// init the app (add the handlers to the Dispatcher)
|
|
27
|
+
/** @type {function} */
|
|
28
|
+
const appStart = await container.get('App_Plugin_Start$');
|
|
29
|
+
await appStart();
|
|
30
|
+
|
|
31
|
+
// order handlers in the Dispatcher
|
|
32
|
+
/** @type {Fl32_Web_Back_Dispatcher} */
|
|
33
|
+
const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
|
|
34
|
+
dispatcher.orderHandlers();
|
|
35
|
+
|
|
36
|
+
// configure and run the server
|
|
37
|
+
/** @type {Fl32_Web_Back_Server} */
|
|
38
|
+
const server = await container.get('Fl32_Web_Back_Server$');
|
|
39
|
+
/** @type {typeof Fl32_Web_Back_Enum_Server_Type} */
|
|
40
|
+
const SERVER_TYPE = await container.get('Fl32_Web_Back_Enum_Server_Type$');
|
|
41
|
+
/** @type {Fl32_Web_Back_Server_Config} */
|
|
42
|
+
const factConfig = await container.get('Fl32_Web_Back_Server_Config$');
|
|
43
|
+
|
|
44
|
+
// Read TLS certificates
|
|
45
|
+
const certsDir = join(cur, '..', 'certs');
|
|
46
|
+
const key = readFileSync(join(certsDir, 'key.pem'), 'utf8');
|
|
47
|
+
const cert = readFileSync(join(certsDir, 'cert.pem'), 'utf8');
|
|
48
|
+
let ca;
|
|
49
|
+
try {
|
|
50
|
+
ca = readFileSync(join(certsDir, 'ca.pem'), 'utf8');
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// CA certificate is optional
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log('Starting HTTPS server on port 3443...');
|
|
56
|
+
const cfg = factConfig.create({
|
|
57
|
+
port: 3443,
|
|
58
|
+
type: SERVER_TYPE.HTTPS,
|
|
59
|
+
tls: {
|
|
60
|
+
key,
|
|
61
|
+
cert,
|
|
62
|
+
ca
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
await server.start(cfg);
|
|
Binary file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
6
|
+
<title>@flancer32/teq-web</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<p>Hello, from `@flancer32/teq-web`!</p>
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {describe, it} from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {buildTestContainer} from '../../../common.js';
|
|
4
|
+
|
|
5
|
+
describe('Fl32_Web_Back_Helper_Order_Kahn', () => {
|
|
6
|
+
|
|
7
|
+
describe('sort', () => {
|
|
8
|
+
it('should sort handlers respecting after/before constraints', async () => {
|
|
9
|
+
const container = buildTestContainer();
|
|
10
|
+
const sorter = await container.get('Fl32_Web_Back_Helper_Order_Kahn$');
|
|
11
|
+
|
|
12
|
+
// Mock handlers with dependencies
|
|
13
|
+
const mk = (name, after = [], before = []) => ({
|
|
14
|
+
getRegistrationInfo: () => ({name, after, before})
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const a = mk('a'); // no deps
|
|
18
|
+
const b = mk('b', ['a']); // after a
|
|
19
|
+
const c = mk('c', [], ['b']); // before b (== b after c)
|
|
20
|
+
const d = mk('d', ['b', 'c']); // after b and c
|
|
21
|
+
|
|
22
|
+
const result = sorter.sort([a, b, c, d]);
|
|
23
|
+
const names = result.map(h => h.getRegistrationInfo().name);
|
|
24
|
+
assert.deepStrictEqual(names, ['a', 'c', 'b', 'd']);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should detect circular dependency', async () => {
|
|
28
|
+
const container = buildTestContainer();
|
|
29
|
+
const sorter = await container.get('Fl32_Web_Back_Helper_Order_Kahn$');
|
|
30
|
+
|
|
31
|
+
const mk = (name, after = [], before = []) => ({
|
|
32
|
+
getRegistrationInfo: () => ({name, after, before})
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const x = mk('x', ['y']);
|
|
36
|
+
const y = mk('y', ['x']);
|
|
37
|
+
|
|
38
|
+
assert.throws(() => {
|
|
39
|
+
sorter.sort([x, y]);
|
|
40
|
+
}, /Circular dependency detected/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should ignore references to unknown handlers', async () => {
|
|
44
|
+
const container = buildTestContainer();
|
|
45
|
+
const sorter = await container.get('Fl32_Web_Back_Helper_Order_Kahn$');
|
|
46
|
+
|
|
47
|
+
const mk = (name, after = [], before = []) => ({
|
|
48
|
+
getRegistrationInfo: () => ({name, after, before})
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const a = mk('a', ['ghost']); // ghost does not exist
|
|
52
|
+
const b = mk('b');
|
|
53
|
+
|
|
54
|
+
const result = sorter.sort([a, b]);
|
|
55
|
+
const names = result.map(h => h.getRegistrationInfo().name);
|
|
56
|
+
assert.deepStrictEqual(names.sort(), ['a', 'b']); // Order can vary, but no crash
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {describe, it, beforeEach} from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {buildTestContainer} from '../common.js';
|
|
4
|
+
|
|
5
|
+
describe('Fl32_Web_Back_Server (mocked)', () => {
|
|
6
|
+
|
|
7
|
+
/** @type {import('@teqfw/di').Container} */
|
|
8
|
+
let container;
|
|
9
|
+
/** @type {Array<*>} */
|
|
10
|
+
const log = [];
|
|
11
|
+
|
|
12
|
+
// Mocks for HTTP/1 and HTTP/2 servers
|
|
13
|
+
const mockHttp = {
|
|
14
|
+
createServer: () => ({
|
|
15
|
+
listen: () => { log.push('http.listen'); },
|
|
16
|
+
on: () => { log.push('http.on'); },
|
|
17
|
+
close: (cb) => { log.push('http.close'); cb && cb(); },
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const mockHttp2 = {
|
|
22
|
+
createServer: () => ({
|
|
23
|
+
listen: () => { log.push('http2.listen'); },
|
|
24
|
+
on: () => { log.push('http2.on'); },
|
|
25
|
+
close: (cb) => { log.push('http2.close'); cb && cb(); },
|
|
26
|
+
}),
|
|
27
|
+
createSecureServer: (tlsOpts) => ({
|
|
28
|
+
listen: () => { log.push('http2s.listen'); },
|
|
29
|
+
on: () => { log.push('http2s.on'); },
|
|
30
|
+
close: (cb) => { log.push('http2s.close'); cb && cb(); },
|
|
31
|
+
})
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
log.length = 0;
|
|
36
|
+
container = buildTestContainer();
|
|
37
|
+
|
|
38
|
+
container.register('node:http', mockHttp);
|
|
39
|
+
container.register('node:http2', mockHttp2);
|
|
40
|
+
|
|
41
|
+
container.register('Fl32_Web_Back_Logger$', {
|
|
42
|
+
info: (...args) => log.push(['info', ...args]),
|
|
43
|
+
error: (...args) => log.push(['error', ...args]),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
container.register('Fl32_Web_Back_Dispatcher$', {
|
|
47
|
+
orderHandlers: () => log.push('dispatcher.orderHandlers'),
|
|
48
|
+
onEventRequest: () => {},
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should start in HTTP/1 mode by default', async () => {
|
|
53
|
+
const server = await container.get('Fl32_Web_Back_Server$');
|
|
54
|
+
await server.start(); // default mode is HTTP/1
|
|
55
|
+
assert.deepStrictEqual(log, [
|
|
56
|
+
'dispatcher.orderHandlers',
|
|
57
|
+
['info', 'Starting server in HTTP/1 mode on port 3000...'],
|
|
58
|
+
'http.on',
|
|
59
|
+
'http.listen',
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should start in HTTP/2 mode if specified', async () => {
|
|
64
|
+
const server = await container.get('Fl32_Web_Back_Server$');
|
|
65
|
+
await server.start({type: 'http2', port: 8080});
|
|
66
|
+
assert.deepStrictEqual(log, [
|
|
67
|
+
'dispatcher.orderHandlers',
|
|
68
|
+
['info', 'Starting server in HTTP/2 mode on port 8080...'],
|
|
69
|
+
'http2.on',
|
|
70
|
+
'http2.listen',
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should start in HTTPS/2 mode with TLS config', async () => {
|
|
75
|
+
const server = await container.get('Fl32_Web_Back_Server$');
|
|
76
|
+
await server.start({type: 'https', port: 8443, tls: {key: 'a', cert: 'b'}});
|
|
77
|
+
assert.deepStrictEqual(log, [
|
|
78
|
+
'dispatcher.orderHandlers',
|
|
79
|
+
['info', 'Starting server in HTTPS (HTTP/2 + TLS) mode on port 8443...'],
|
|
80
|
+
'http2s.on',
|
|
81
|
+
'http2s.listen',
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should throw error if TLS config is missing in HTTPS mode', async () => {
|
|
86
|
+
const server = await container.get('Fl32_Web_Back_Server$');
|
|
87
|
+
await assert.rejects(
|
|
88
|
+
() => server.start({type: 'https', port: 1234}),
|
|
89
|
+
/TLS key and certificate are required/
|
|
90
|
+
);
|
|
91
|
+
assert.deepStrictEqual(log.at(-1), ['error', 'HTTPS server requires TLS key and certificate']);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should throw error on unsupported server type', async () => {
|
|
95
|
+
const server = await container.get('Fl32_Web_Back_Server$');
|
|
96
|
+
await assert.rejects(
|
|
97
|
+
() => server.start({type: 'ftp', port: 21}),
|
|
98
|
+
/not supported/
|
|
99
|
+
);
|
|
100
|
+
assert.deepStrictEqual(log.at(-1), ['error', 'Unsupported server type: ftp']);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should stop the server', async () => {
|
|
104
|
+
const server = await container.get('Fl32_Web_Back_Server$');
|
|
105
|
+
await server.start();
|
|
106
|
+
await server.stop();
|
|
107
|
+
assert.deepStrictEqual(log.slice(-2), [
|
|
108
|
+
'http.close',
|
|
109
|
+
['info', 'Server stopped'],
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides a utility to create a configured TeqFW DI container for unit testing.
|
|
3
|
+
*/
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import Container from '@teqfw/di';
|
|
6
|
+
|
|
7
|
+
// Resolve the plugin source path relative to this script
|
|
8
|
+
const SRC = path.resolve(import.meta.dirname, '../../src');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds a test DI container for unit tests.
|
|
12
|
+
* Registers plugin namespace and enables test mode.
|
|
13
|
+
*
|
|
14
|
+
* @returns {TeqFw_Di_Container} Test container instance.
|
|
15
|
+
*/
|
|
16
|
+
export function buildTestContainer() {
|
|
17
|
+
const container = new Container();
|
|
18
|
+
const resolver = container.getResolver();
|
|
19
|
+
resolver.addNamespaceRoot('Fl32_Web_', SRC);
|
|
20
|
+
container.enableTestMode();
|
|
21
|
+
return container;
|
|
22
|
+
}
|