web_plsql 0.5.1 → 0.8.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 +128 -111
- package/examples/server_apex.js +26 -0
- package/examples/server_sample.js +30 -0
- package/examples/sql/{doc_table.sql → doctable.sql} +1 -1
- package/examples/sql/install.sql +6 -9
- package/examples/sql/sample_package.sql +27 -0
- package/examples/sql/sample_package_body.sql +245 -0
- package/examples/static/sample.css +11 -10
- package/package.json +92 -83
- package/src/cgi.js +127 -0
- package/src/error.js +24 -0
- package/src/errorPage.js +296 -0
- package/src/file.js +95 -0
- package/src/handlerLogger.js +21 -0
- package/src/handlerMetrics.js +46 -0
- package/src/handlerPlSql.js +65 -0
- package/src/handlerUpload.js +32 -0
- package/src/index.js +17 -0
- package/src/oracle.js +80 -0
- package/src/parsePage.js +193 -0
- package/src/procedure.js +260 -0
- package/src/procedureError.js +40 -0
- package/src/procedureNamed.js +233 -0
- package/src/procedureSanitize.js +215 -0
- package/src/procedureVariable.js +57 -0
- package/src/request.js +103 -0
- package/src/{requestError.ts → requestError.js} +8 -4
- package/src/sendResponse.js +104 -0
- package/src/server.js +185 -0
- package/src/shutdown.js +53 -0
- package/src/stream.js +28 -0
- package/src/trace.js +74 -0
- package/src/tty.js +48 -0
- package/src/types.js +146 -0
- package/src/upload.js +92 -0
- package/src/version.js +38 -0
- package/types/cgi.d.ts +4 -0
- package/types/error.d.ts +1 -0
- package/types/errorPage.d.ts +10 -0
- package/types/file.d.ts +6 -0
- package/types/handlerLogger.d.ts +2 -0
- package/types/handlerMetrics.d.ts +4 -0
- package/types/handlerPlSql.d.ts +8 -0
- package/types/handlerUpload.d.ts +7 -0
- package/types/index.d.ts +10 -0
- package/types/oracle.d.ts +5 -0
- package/types/parsePage.d.ts +3 -0
- package/types/procedure.d.ts +10 -0
- package/types/procedureError.d.ts +23 -0
- package/types/procedureNamed.d.ts +14 -0
- package/types/procedureSanitize.d.ts +14 -0
- package/types/procedureVariable.d.ts +9 -0
- package/types/request.d.ts +7 -0
- package/types/requestError.d.ts +8 -0
- package/types/sendResponse.d.ts +4 -0
- package/types/server.d.ts +11 -0
- package/types/shutdown.d.ts +2 -0
- package/types/stream.d.ts +1 -0
- package/types/trace.d.ts +5 -0
- package/types/tty.d.ts +4 -0
- package/types/types.d.ts +251 -0
- package/types/upload.d.ts +5 -0
- package/types/version.d.ts +8 -0
- package/.editorconfig +0 -8
- package/.eslintignore +0 -3
- package/.eslintrc.js +0 -347
- package/CHANGELOG.md +0 -133
- package/examples/apex.js +0 -70
- package/examples/credentials.js +0 -22
- package/examples/oracledb_example.js +0 -30
- package/examples/sample.js +0 -101
- package/examples/sql/sample.pkb +0 -223
- package/examples/sql/sample.pks +0 -24
- package/jest.config.js +0 -207
- package/src/cgi.ts +0 -95
- package/src/config.ts +0 -97
- package/src/errorPage.ts +0 -286
- package/src/fileUpload.ts +0 -126
- package/src/index.ts +0 -65
- package/src/page.ts +0 -275
- package/src/procedure.ts +0 -360
- package/src/procedureError.ts +0 -27
- package/src/request.ts +0 -139
- package/src/stream.ts +0 -26
- package/src/trace.ts +0 -194
- package/test/.eslintrc.json +0 -5
- package/test/__tests__/cgi.ts +0 -96
- package/test/__tests__/config.ts +0 -41
- package/test/__tests__/errorPage.ts +0 -101
- package/test/__tests__/oracledb_mock.ts +0 -98
- package/test/__tests__/server.ts +0 -495
- package/test/__tests__/stream.ts +0 -21
- package/test/mock/oracledb.ts +0 -85
- package/test/static/static.html +0 -1
- package/tsconfig.json +0 -24
- package/tsconfig.src.json +0 -23
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import debugModule from 'debug';
|
|
2
|
+
const debug = debugModule('webplsql:procedureSanitize');
|
|
3
|
+
|
|
4
|
+
import oracledb from 'oracledb';
|
|
5
|
+
import z from 'zod';
|
|
6
|
+
import {RequestError} from './requestError.js';
|
|
7
|
+
import {errorToString} from './error.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import('express').Request} Request
|
|
11
|
+
* @typedef {import('express').Response} Response
|
|
12
|
+
* @typedef {import('oracledb').Connection} Connection
|
|
13
|
+
* @typedef {import('oracledb').Result<unknown>} Result
|
|
14
|
+
* @typedef {import('./types.js').argObjType} argObjType
|
|
15
|
+
* @typedef {import('./types.js').fileUploadType} fileUploadType
|
|
16
|
+
* @typedef {import('./types.js').environmentType} environmentType
|
|
17
|
+
* @typedef {import('./types.js').configPlSqlHandlerType} configPlSqlHandlerType
|
|
18
|
+
* @typedef {import('./types.js').BindParameterConfig} BindParameterConfig
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DEFAULT_EXCLUSION_LIST = ['sys.', 'dbms_', 'utl_', 'owa_', 'htp.', 'htf.', 'wpg_docload.', 'ctxsys.', 'mdsys.'];
|
|
22
|
+
|
|
23
|
+
// NOTE: Consider using a separate cache for each database pool to avoid possible conflicts.
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {{hitCount: number, valid: boolean}} cacheEntryType
|
|
26
|
+
*/
|
|
27
|
+
/** @type {Map<string, cacheEntryType>} */
|
|
28
|
+
const REQUEST_VALIDATION_FUNCTION_CACHE = new Map();
|
|
29
|
+
const REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT = 10000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sanitize the procedure name.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} procName - The procedure name.
|
|
35
|
+
* @param {Connection} databaseConnection - The database connection
|
|
36
|
+
* @param {configPlSqlHandlerType} options - the options for the middleware.
|
|
37
|
+
* @returns {Promise<string>} Promise resolving to final procedure name.
|
|
38
|
+
*/
|
|
39
|
+
export const sanitizeProcName = async (procName, databaseConnection, options) => {
|
|
40
|
+
debug('sanitizeProcName', procName);
|
|
41
|
+
|
|
42
|
+
// make lowercase and trim
|
|
43
|
+
let finalProcName = procName.toLowerCase().trim();
|
|
44
|
+
|
|
45
|
+
// remove special characters
|
|
46
|
+
finalProcName = removeSpecialCharacters(finalProcName);
|
|
47
|
+
|
|
48
|
+
// check for default exclusions
|
|
49
|
+
for (const i of DEFAULT_EXCLUSION_LIST) {
|
|
50
|
+
if (finalProcName.startsWith(i)) {
|
|
51
|
+
const error = `Procedure name "${procName}" is in default exclusion list "${DEFAULT_EXCLUSION_LIST.join(',')}"`;
|
|
52
|
+
debug(error);
|
|
53
|
+
throw new RequestError(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// check for custom exclusions
|
|
58
|
+
if (options.exclusionList && options.exclusionList.length > 0) {
|
|
59
|
+
for (const i of options.exclusionList) {
|
|
60
|
+
if (finalProcName.startsWith(i)) {
|
|
61
|
+
const error = `Procedure name "${procName}" is in custom exclusion list "${options.exclusionList.join(',')}"`;
|
|
62
|
+
debug(error);
|
|
63
|
+
throw new RequestError(error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check request validation function
|
|
69
|
+
if (options.requestValidationFunction && options.requestValidationFunction.length > 0) {
|
|
70
|
+
const valid = await requestValidationFunction(finalProcName, options.requestValidationFunction, databaseConnection);
|
|
71
|
+
if (!valid) {
|
|
72
|
+
const error = `Procedure name "${procName}" is not valid according to the request validation function "${options.requestValidationFunction}"`;
|
|
73
|
+
debug(error);
|
|
74
|
+
throw new RequestError(error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return finalProcName;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} str - string
|
|
83
|
+
* @returns {string} - string
|
|
84
|
+
*/
|
|
85
|
+
const removeSpecialCharacters = (str) => {
|
|
86
|
+
if (str === null || str === undefined) {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const chars = [];
|
|
91
|
+
|
|
92
|
+
for (const c of str) {
|
|
93
|
+
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c === '.' || c === '_' || c === '#' || c === '$') {
|
|
94
|
+
chars.push(c);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return chars.join('');
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} procName - The procedure name.
|
|
103
|
+
* @param {string} requestValidationFunction - The request validation function.
|
|
104
|
+
* @param {Connection} databaseConnection - The database connection
|
|
105
|
+
* @returns {Promise<boolean>} Promise resolving to final procedure name.
|
|
106
|
+
*/
|
|
107
|
+
const loadRequestValid = async (procName, requestValidationFunction, databaseConnection) => {
|
|
108
|
+
/** @type {BindParameterConfig} */
|
|
109
|
+
const bind = {
|
|
110
|
+
proc: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: procName},
|
|
111
|
+
valid: {dir: oracledb.BIND_OUT, type: oracledb.NUMBER},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const SQL = [
|
|
115
|
+
'DECLARE',
|
|
116
|
+
' l_valid NUMBER := 0;',
|
|
117
|
+
'BEGIN',
|
|
118
|
+
` IF (${requestValidationFunction}(:proc)) THEN`,
|
|
119
|
+
' l_valid := 1;',
|
|
120
|
+
' END IF;',
|
|
121
|
+
' :valid := l_valid;',
|
|
122
|
+
'END;',
|
|
123
|
+
].join('\n');
|
|
124
|
+
|
|
125
|
+
/** @type {Result} */
|
|
126
|
+
let result = {};
|
|
127
|
+
try {
|
|
128
|
+
result = await databaseConnection.execute(SQL, bind);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
debug('result', result);
|
|
131
|
+
/* istanbul ignore next */
|
|
132
|
+
const message = `Error when validating procedure name "${procName}"\n${SQL}\n${errorToString(err)}`;
|
|
133
|
+
/* istanbul ignore next */
|
|
134
|
+
throw new RequestError(message);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const data = z.object({valid: z.number()}).strict().parse(result.outBinds);
|
|
139
|
+
return data.valid === 1;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
debug('result', result.outBinds);
|
|
142
|
+
/* istanbul ignore next */
|
|
143
|
+
const message = `Internal error when parsing ${result.outBinds}\n${errorToString(err)}`;
|
|
144
|
+
/* istanbul ignore next */
|
|
145
|
+
throw new Error(message);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Remove the cache entries with the lowest hitCount.
|
|
151
|
+
* @param {number} count - Number of entries to remove
|
|
152
|
+
* @returns {void}
|
|
153
|
+
*/
|
|
154
|
+
const removeLowestHitCountEntries = (count) => {
|
|
155
|
+
// Convert cache entries to an array
|
|
156
|
+
const entries = Array.from(REQUEST_VALIDATION_FUNCTION_CACHE.entries());
|
|
157
|
+
|
|
158
|
+
// Sort entries by hitCount in ascending order
|
|
159
|
+
entries.sort((a, b) => a[1].hitCount - b[1].hitCount);
|
|
160
|
+
|
|
161
|
+
// Get the keys of the `count` entries with the lowest hitCount
|
|
162
|
+
const keysToRemove = entries.slice(0, count).map(([key]) => key);
|
|
163
|
+
|
|
164
|
+
// Remove these entries from the cache
|
|
165
|
+
for (const key of keysToRemove) {
|
|
166
|
+
REQUEST_VALIDATION_FUNCTION_CACHE.delete(key);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Request validation function.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} procName - The procedure name.
|
|
174
|
+
* @param {string} requestValidationFunction - The request validation function.
|
|
175
|
+
* @param {Connection} databaseConnection - The database connection
|
|
176
|
+
* @returns {Promise<boolean>} Promise resolving to final procedure name.
|
|
177
|
+
*/
|
|
178
|
+
const requestValidationFunction = async (procName, requestValidationFunction, databaseConnection) => {
|
|
179
|
+
debug('requestValidationFunction', procName, requestValidationFunction);
|
|
180
|
+
|
|
181
|
+
// calculate the key
|
|
182
|
+
//const key = `${databaseConnection.connectString}_${databaseConnection.user}_${procName.toLowerCase()}`;
|
|
183
|
+
const key = procName.toLowerCase();
|
|
184
|
+
|
|
185
|
+
// lookup in the cache
|
|
186
|
+
const cacheEntry = REQUEST_VALIDATION_FUNCTION_CACHE.get(key);
|
|
187
|
+
|
|
188
|
+
// if we fount the procedure in the cache, we increase the hit cound and return
|
|
189
|
+
if (cacheEntry) {
|
|
190
|
+
cacheEntry.hitCount++;
|
|
191
|
+
if (debug.enabled) {
|
|
192
|
+
debug(`findArguments: procedure "${procName}" found in cache with "${cacheEntry.hitCount}" hits`);
|
|
193
|
+
}
|
|
194
|
+
return cacheEntry.valid;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// if the cache is full, we remove the 1000 least used cache entries
|
|
198
|
+
if (REQUEST_VALIDATION_FUNCTION_CACHE.size > REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT) {
|
|
199
|
+
if (debug.enabled) {
|
|
200
|
+
debug(`findArguments: cache is full. size=${REQUEST_VALIDATION_FUNCTION_CACHE.size} max=${REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT}`);
|
|
201
|
+
}
|
|
202
|
+
removeLowestHitCountEntries(1000);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// load from database
|
|
206
|
+
if (debug.enabled) {
|
|
207
|
+
debug(`findArguments: procedure "${procName}" not found in cache and must be loaded`);
|
|
208
|
+
}
|
|
209
|
+
const valid = await loadRequestValid(procName, requestValidationFunction, databaseConnection);
|
|
210
|
+
|
|
211
|
+
// add to the cache
|
|
212
|
+
REQUEST_VALIDATION_FUNCTION_CACHE.set(key, {hitCount: 0, valid});
|
|
213
|
+
|
|
214
|
+
return valid;
|
|
215
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Invoke the Oracle procedure and return the raw content of the page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import debugModule from 'debug';
|
|
6
|
+
const debug = debugModule('webplsql:procedureVariable');
|
|
7
|
+
|
|
8
|
+
import oracledb from 'oracledb';
|
|
9
|
+
import {sanitizeProcName} from './procedureSanitize.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('oracledb').Connection} Connection
|
|
13
|
+
* @typedef {import('oracledb').Result<unknown>} Result
|
|
14
|
+
* @typedef {import('./types.js').configPlSqlHandlerType} configPlSqlHandlerType
|
|
15
|
+
* @typedef {import('./types.js').argObjType} argObjType
|
|
16
|
+
* @typedef {import('./types.js').BindParameterConfig} BindParameterConfig
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the sql statement and bindings for the procedure to execute for a variable number of arguments
|
|
21
|
+
* @param {string} procName - The procedure to execute
|
|
22
|
+
* @param {argObjType} argObj - The arguments to pass to the procedure
|
|
23
|
+
* @param {Connection} databaseConnection - The database connection
|
|
24
|
+
* @param {configPlSqlHandlerType} options - the options for the middleware.
|
|
25
|
+
* @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
|
|
26
|
+
*/
|
|
27
|
+
export const getProcedureVariable = async (procName, argObj, databaseConnection, options) => {
|
|
28
|
+
if (debug.enabled) {
|
|
29
|
+
debug(`getProcedureVariable: ${procName} arguments=`, argObj);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const names = [];
|
|
33
|
+
const values = [];
|
|
34
|
+
|
|
35
|
+
for (const key in argObj) {
|
|
36
|
+
const value = argObj[key];
|
|
37
|
+
if (typeof value === 'string') {
|
|
38
|
+
names.push(key);
|
|
39
|
+
values.push(value);
|
|
40
|
+
} else if (Array.isArray(value)) {
|
|
41
|
+
value.forEach((item) => {
|
|
42
|
+
names.push(key);
|
|
43
|
+
values.push(item);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const sanitizedProcName = await sanitizeProcName(procName, databaseConnection, options);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
sql: `${sanitizedProcName}(:argnames, :argvalues);`,
|
|
52
|
+
bind: {
|
|
53
|
+
argnames: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: names},
|
|
54
|
+
argvalues: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: values},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
};
|
package/src/request.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Process the http request
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import debugModule from 'debug';
|
|
6
|
+
const debug = debugModule('webplsql:request');
|
|
7
|
+
|
|
8
|
+
import util from 'node:util';
|
|
9
|
+
import {invokeProcedure} from './procedure.js';
|
|
10
|
+
import {getCGI} from './cgi.js';
|
|
11
|
+
import {getFiles} from './upload.js';
|
|
12
|
+
import {RequestError} from './requestError.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {import('express').Request} Request
|
|
16
|
+
* @typedef {import('express').Response} Response
|
|
17
|
+
* @typedef {import('oracledb').Pool} Pool
|
|
18
|
+
* @typedef {import('oracledb').Connection} Connection
|
|
19
|
+
* @typedef {import('./types.js').argObjType} argObjType
|
|
20
|
+
* @typedef {import('./types.js').configPlSqlHandlerType} configPlSqlHandlerType
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute the request
|
|
25
|
+
*
|
|
26
|
+
* @param {Request} req - The req object represents the HTTP request.
|
|
27
|
+
* @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
|
|
28
|
+
* @param {configPlSqlHandlerType} options - the options for the middleware.
|
|
29
|
+
* @param {Pool} connectionPool - The connection pool.
|
|
30
|
+
* @returns {Promise<void>} - Promise resolving to th page
|
|
31
|
+
*/
|
|
32
|
+
export const processRequest = async (req, res, options, connectionPool) => {
|
|
33
|
+
debug('executeRequest: ENTER');
|
|
34
|
+
|
|
35
|
+
// open database connection
|
|
36
|
+
const connection = await connectionPool.getConnection();
|
|
37
|
+
|
|
38
|
+
// Get the CGI
|
|
39
|
+
const cgiObj = getCGI(req, options.documentTable ?? '', options.cgi ?? {});
|
|
40
|
+
debug('executeRequest: cgiObj=', cgiObj);
|
|
41
|
+
|
|
42
|
+
// Does the request contain any files
|
|
43
|
+
const filesToUpload = getFiles(req);
|
|
44
|
+
debug('executeRequest: filesToUpload=', filesToUpload);
|
|
45
|
+
|
|
46
|
+
// Add the query properties
|
|
47
|
+
/** @type {argObjType} */
|
|
48
|
+
const argObj = {};
|
|
49
|
+
Object.assign(argObj, req.query);
|
|
50
|
+
|
|
51
|
+
// Add the files to the arguments
|
|
52
|
+
filesToUpload.reduce((aggregator, file) => {
|
|
53
|
+
aggregator[file.fieldname] = file.originalname;
|
|
54
|
+
return aggregator;
|
|
55
|
+
}, argObj);
|
|
56
|
+
|
|
57
|
+
// Does the request contain a body
|
|
58
|
+
Object.assign(argObj, normalizeBody(req));
|
|
59
|
+
|
|
60
|
+
// invoke the Oracle procedure and get the page contenst
|
|
61
|
+
await invokeProcedure(req, res, argObj, cgiObj, filesToUpload, options, connection);
|
|
62
|
+
|
|
63
|
+
// close database connection
|
|
64
|
+
await connection.release();
|
|
65
|
+
|
|
66
|
+
debug('executeRequest: EXIT');
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Is the given value a string or an array of strings
|
|
71
|
+
* @param {unknown} value - The value to check.
|
|
72
|
+
* @returns {value is string | string[]} - True if the value is a string or an array of strings
|
|
73
|
+
*/
|
|
74
|
+
const isStringOrArrayOfString = (value) => typeof value === 'string' || (Array.isArray(value) && value.every((element) => typeof element === 'string'));
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Normalize the body by making sure that only "simple" parameters and no nested objects are submitted
|
|
78
|
+
* @param {Request} req - The req object represents the HTTP request.
|
|
79
|
+
* @returns {Record<string, string | string[]>} - The normalized body.
|
|
80
|
+
*/
|
|
81
|
+
const normalizeBody = (req) => {
|
|
82
|
+
/** @type {Record<string, string | string[]>} */
|
|
83
|
+
const args = {};
|
|
84
|
+
|
|
85
|
+
/* istanbul ignore else */
|
|
86
|
+
if (typeof req.body === 'object' && req.body !== null) {
|
|
87
|
+
for (const key in req.body) {
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
89
|
+
const value = req.body[key];
|
|
90
|
+
/* istanbul ignore else */
|
|
91
|
+
if (isStringOrArrayOfString(value)) {
|
|
92
|
+
args[key] = value;
|
|
93
|
+
} else {
|
|
94
|
+
/* istanbul ignore next */
|
|
95
|
+
throw new RequestError(
|
|
96
|
+
`The element "${key}" in the body is not a string or an array of strings!\n${util.inspect(req.body, {showHidden: false, depth: null, colors: false})}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return args;
|
|
103
|
+
};
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* RequestError
|
|
3
|
-
*/
|
|
2
|
+
* RequestError
|
|
3
|
+
*/
|
|
4
4
|
|
|
5
5
|
export class RequestError extends Error {
|
|
6
|
-
|
|
6
|
+
/** @type {Date} */
|
|
7
|
+
timestamp;
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} message - The error message.
|
|
11
|
+
*/
|
|
12
|
+
constructor(message) {
|
|
9
13
|
super(message);
|
|
10
14
|
|
|
11
15
|
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Page the raw page content and return the content to the client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import debugModule from 'debug';
|
|
6
|
+
const debug = debugModule('webplsql:sendResponse');
|
|
7
|
+
|
|
8
|
+
import {getBlock} from './trace.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('express').Request} Request
|
|
12
|
+
* @typedef {import('express').Response} Response
|
|
13
|
+
* @typedef {import('./types.js').pageType} pageType
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Send "default" response to the browser
|
|
18
|
+
* @param {Request} req - The req object represents the HTTP request.
|
|
19
|
+
* @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
|
|
20
|
+
* @param {pageType} page - The page to send.
|
|
21
|
+
* @returns {void}
|
|
22
|
+
*/
|
|
23
|
+
export const sendResponse = (req, res, page) => {
|
|
24
|
+
/** @type {string[]} */
|
|
25
|
+
const debugText = [];
|
|
26
|
+
|
|
27
|
+
// Send the "cookies"
|
|
28
|
+
page.head.cookies.forEach((cookie) => {
|
|
29
|
+
const {name, value} = cookie;
|
|
30
|
+
|
|
31
|
+
if (debug.enabled) {
|
|
32
|
+
debugText.push(`res.cookie("${name}", "${value}")`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
res.cookie(name, value);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// If there is a "redirectLocation" header, we immediately redirect and return
|
|
39
|
+
if (typeof page.head.redirectLocation === 'string' && page.head.redirectLocation.length > 0) {
|
|
40
|
+
if (debug.enabled) {
|
|
41
|
+
debugText.push(`res.redirect(302, "${page.head.redirectLocation}")`);
|
|
42
|
+
debug(getBlock('RESPONSE', debugText.join('\n')));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
res.redirect(302, page.head.redirectLocation);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Send all the "otherHeaders"
|
|
50
|
+
for (const key in page.head.otherHeaders) {
|
|
51
|
+
if (debug.enabled) {
|
|
52
|
+
debugText.push(`res.set("${key}", "${page.head.otherHeaders[key]}")`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
res.set(key, page.head.otherHeaders[key]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// If this is a file download, we eventually set the "Content-Type" and the file content and then return.
|
|
59
|
+
if (page.file.fileType === 'B' || page.file.fileType === 'F') {
|
|
60
|
+
if (typeof page.head.contentType === 'string' && page.head.contentType.length > 0) {
|
|
61
|
+
if (debug.enabled) {
|
|
62
|
+
debugText.push(`res.writeHead("Content-Type", "${page.head.contentType}")`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
res.writeHead(200, {'Content-Type': page.head.contentType});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (debug.enabled) {
|
|
69
|
+
debugText.push(`res.end("${page.file.fileType}")`);
|
|
70
|
+
debug(getBlock('RESPONSE', debugText.join('\n')));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
res.end(page.file.fileBlob, 'binary');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Is the a "contentType" header
|
|
78
|
+
if (typeof page.head.contentType === 'string' && page.head.contentType.length > 0) {
|
|
79
|
+
if (debug.enabled) {
|
|
80
|
+
debugText.push(`res.set("Content-Type", "${page.head.contentType}")`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
res.set('Content-Type', page.head.contentType);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// If we have a "Status" header, we send the header and then return.
|
|
87
|
+
if (typeof page.head.statusCode === 'number') {
|
|
88
|
+
if (debug.enabled) {
|
|
89
|
+
debugText.push(`res.status(page.head.statusCode).send("${page.head.statusDescription}")`);
|
|
90
|
+
debug(getBlock('RESPONSE', debugText.join('\n')));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
res.status(page.head.statusCode).send(page.head.statusDescription);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Send the body
|
|
98
|
+
if (debug.enabled) {
|
|
99
|
+
debugText.push(`${'-'.repeat(60)}\n${page.body}`);
|
|
100
|
+
debug(getBlock('RESPONSE', debugText.join('\n')));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
res.send(page.body);
|
|
104
|
+
};
|
package/src/server.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import debugModule from 'debug';
|
|
2
|
+
const debug = debugModule('webplsql:server');
|
|
3
|
+
|
|
4
|
+
import http from 'node:http';
|
|
5
|
+
import https from 'node:https';
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import bodyParser from 'body-parser';
|
|
8
|
+
import cookieParser from 'cookie-parser';
|
|
9
|
+
import compression from 'compression';
|
|
10
|
+
import {z$configType} from './types.js';
|
|
11
|
+
import {installShutdown} from './shutdown.js';
|
|
12
|
+
import {writeAfterEraseLine} from './tty.js';
|
|
13
|
+
import {poolCreate, poolsClose} from '../src/oracle.js';
|
|
14
|
+
import {handlerUpload} from './handlerUpload.js';
|
|
15
|
+
import {handlerLogger} from './handlerLogger.js';
|
|
16
|
+
import {initMetrics, handlerMetrics} from './handlerMetrics.js';
|
|
17
|
+
import {handlerWebPlSql} from './handlerPlSql.js';
|
|
18
|
+
import {getPackageVersion, getExpressVersion} from './version.js';
|
|
19
|
+
import {readFileSyncUtf8, getJsonFile} from './file.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {import('express').Express} Express
|
|
23
|
+
* @typedef {import('express').Request} Request
|
|
24
|
+
* @typedef {import('express').Response} Response
|
|
25
|
+
* @typedef {import('express').NextFunction} NextFunction
|
|
26
|
+
* @typedef {import('oracledb').Pool} Pool
|
|
27
|
+
* @typedef {import('./types.js').environmentType} environmentType
|
|
28
|
+
* @typedef {import('./types.js').configType} configType
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create HTTP server.
|
|
33
|
+
* @param {Express} app - express application
|
|
34
|
+
* @param {number} port - port number
|
|
35
|
+
* @param {Pool[]} connectionPools - database connection
|
|
36
|
+
* @returns {Promise<http.Server>} - server
|
|
37
|
+
*/
|
|
38
|
+
export const createHttpServer = (app, port, connectionPools) => {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
// Create server
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
42
|
+
const server = http.createServer({}, app);
|
|
43
|
+
|
|
44
|
+
// Install shutdown handler
|
|
45
|
+
installShutdown(async () => {
|
|
46
|
+
// Close database pool.
|
|
47
|
+
await poolsClose(connectionPools);
|
|
48
|
+
|
|
49
|
+
// Close server
|
|
50
|
+
return new Promise((resolve) => server.close(() => resolve()));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Listen on HTTP ports
|
|
54
|
+
server.listen(port, () => {
|
|
55
|
+
resolve(server);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create HTTPS server.
|
|
62
|
+
* @param {Express} app - express application
|
|
63
|
+
* @param {string} sslKeyFilename - ssl
|
|
64
|
+
* @param {string} sslCertFilename - ssl
|
|
65
|
+
* @param {number} port - port number
|
|
66
|
+
* @param {Pool[]} connectionPools - database connection
|
|
67
|
+
* @returns {Promise<https.Server>} - server
|
|
68
|
+
*/
|
|
69
|
+
export const createHttpsServer = (app, sslKeyFilename, sslCertFilename, port, connectionPools) => {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
// Load certificates
|
|
72
|
+
const key = readFileSyncUtf8(sslKeyFilename);
|
|
73
|
+
const cert = readFileSyncUtf8(sslCertFilename);
|
|
74
|
+
|
|
75
|
+
// Create server
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
77
|
+
const server = https.createServer({key, cert}, app);
|
|
78
|
+
|
|
79
|
+
// Install shutdown handler
|
|
80
|
+
installShutdown(async () => {
|
|
81
|
+
// Close database pool.
|
|
82
|
+
await poolsClose(connectionPools);
|
|
83
|
+
|
|
84
|
+
// Close server
|
|
85
|
+
return new Promise((resolve) => server.close(() => resolve()));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Listen on HTTP ports
|
|
89
|
+
server.listen(port, () => {
|
|
90
|
+
resolve(server);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Start HTTP server.
|
|
97
|
+
* @param {configType} config - The config.
|
|
98
|
+
* @returns {Promise<void>} - Promise.
|
|
99
|
+
*/
|
|
100
|
+
export const startHttpServer = async (config) => {
|
|
101
|
+
debug('startHttpServer', config);
|
|
102
|
+
|
|
103
|
+
const expressVersion = getExpressVersion();
|
|
104
|
+
const packageVersion = getPackageVersion();
|
|
105
|
+
|
|
106
|
+
config = z$configType.parse(config);
|
|
107
|
+
|
|
108
|
+
console.log(`WEB_PL/SQL ${packageVersion} using express ${expressVersion}`);
|
|
109
|
+
|
|
110
|
+
// Create express app
|
|
111
|
+
const app = express();
|
|
112
|
+
|
|
113
|
+
// Metrics
|
|
114
|
+
const metrics = initMetrics();
|
|
115
|
+
app.use(handlerMetrics(metrics));
|
|
116
|
+
|
|
117
|
+
// Serving static files
|
|
118
|
+
for (const i of config.routeStatic) {
|
|
119
|
+
app.use(i.route, express.static(i.directoryPath));
|
|
120
|
+
console.log(`Static resources on "${i.route}" from directory ${i.directoryPath}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Default middleware
|
|
124
|
+
app.use(handlerUpload());
|
|
125
|
+
app.use(bodyParser.json());
|
|
126
|
+
app.use(bodyParser.urlencoded({extended: true}));
|
|
127
|
+
app.use(cookieParser());
|
|
128
|
+
app.use(compression());
|
|
129
|
+
|
|
130
|
+
// Access log
|
|
131
|
+
if (config.loggerFilename.length > 0) {
|
|
132
|
+
console.log(`Access log in "${config.loggerFilename}"`);
|
|
133
|
+
app.use(handlerLogger(config.loggerFilename));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @type {Pool[]} */
|
|
137
|
+
const connectionPools = [];
|
|
138
|
+
|
|
139
|
+
// Oracle pl/sql express middleware
|
|
140
|
+
for (const i of config.routePlSql) {
|
|
141
|
+
// Allocate the Oracle database pool
|
|
142
|
+
const pool = await poolCreate(i.user, i.password, i.connectString);
|
|
143
|
+
connectionPools.push(pool);
|
|
144
|
+
|
|
145
|
+
app.use([`${i.route}/:name`, i.route], handlerWebPlSql(pool, i));
|
|
146
|
+
|
|
147
|
+
console.log(`Application route "http://localhost:${config.port}${i.route}"`);
|
|
148
|
+
console.log(` Oracle user: ${i.user}`);
|
|
149
|
+
console.log(` Oracle server: ${i.connectString}`);
|
|
150
|
+
console.log(` Oracle document table: ${i.documentTable}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Update metrics every second
|
|
154
|
+
if (config.monitorConsole) {
|
|
155
|
+
setInterval(() => {
|
|
156
|
+
// Update requests per second
|
|
157
|
+
const requestsLastInterval = metrics.requestsInLastInterval;
|
|
158
|
+
metrics.requestsInLastInterval = 0;
|
|
159
|
+
|
|
160
|
+
// Clear console and display metrics
|
|
161
|
+
writeAfterEraseLine(`Total requests: ${metrics.totalRequests}, requests per second: ${requestsLastInterval}`);
|
|
162
|
+
}, 1000);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await createHttpServer(app, config.port, connectionPools);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Load configuration.
|
|
170
|
+
* @param {string} [filename] - The configuration filename.
|
|
171
|
+
* @returns {configType} - Promise.
|
|
172
|
+
*/
|
|
173
|
+
export const loadConfig = (filename) => {
|
|
174
|
+
debug('loadConfig', filename);
|
|
175
|
+
|
|
176
|
+
if (typeof filename !== 'string' || filename.length === 0) {
|
|
177
|
+
filename = 'config.json';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const data = getJsonFile(filename);
|
|
181
|
+
|
|
182
|
+
const config = z$configType.parse(data);
|
|
183
|
+
|
|
184
|
+
return config;
|
|
185
|
+
};
|