web_plsql 0.18.0 → 1.0.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 +24 -4
- package/package.json +15 -5
- package/src/admin/client/charts.ts +299 -0
- package/src/admin/client/main.js +3 -0
- package/src/admin/client/tailwind.css +41 -0
- package/src/admin/favicon.svg +3 -0
- package/src/admin/index.html +315 -0
- package/src/admin/js/api.ts +95 -0
- package/src/admin/js/app.ts +306 -0
- package/src/admin/js/eslint.config.js +74 -0
- package/src/admin/js/schemas.ts +153 -0
- package/src/admin/js/templates/config.ts +146 -0
- package/src/admin/js/templates/errorRow.ts +18 -0
- package/src/admin/js/templates/index.ts +3 -0
- package/src/admin/js/templates/poolCard.ts +61 -0
- package/src/admin/js/tsconfig.json +24 -0
- package/src/admin/js/types.ts +223 -0
- package/src/admin/js/ui/theme.ts +93 -0
- package/src/admin/js/ui/views.ts +164 -0
- package/src/admin/js/util/format.ts +27 -0
- package/src/admin/lib/assets/main-zpdhQ1gD.css +1 -0
- package/src/admin/lib/chart.bundle.js +139 -0
- package/src/admin/style.css +1321 -0
- package/src/bin/load-test.js +202 -0
- package/src/handler/handlerAdmin.js +198 -0
- package/src/handler/plsql/cgi.js +1 -1
- package/src/handler/plsql/errorPage.js +35 -6
- package/src/handler/plsql/handlerPlSql.js +32 -6
- package/src/handler/plsql/parsePage.js +7 -3
- package/src/handler/plsql/procedure.js +57 -11
- package/src/handler/plsql/procedureNamed.js +18 -47
- package/src/handler/plsql/procedureSanitize.js +96 -45
- package/src/handler/plsql/procedureVariable.js +2 -2
- package/src/handler/plsql/request.js +8 -3
- package/src/handler/plsql/sendResponse.js +2 -2
- package/src/server/config.js +1 -0
- package/src/server/server.js +108 -2
- package/src/types.js +6 -0
- package/src/util/cache.js +123 -0
- package/src/util/jsonLogger.js +47 -0
- package/src/util/shutdown.js +6 -2
- package/src/util/trace.js +5 -5
- package/src/version.js +1 -1
- package/types/handler/handlerAdmin.d.ts +9 -0
- package/types/handler/plsql/errorPage.d.ts +1 -0
- package/types/handler/plsql/handlerPlSql.d.ts +6 -1
- package/types/handler/plsql/procedure.d.ts +3 -1
- package/types/handler/plsql/procedureNamed.d.ts +2 -5
- package/types/handler/plsql/procedureSanitize.d.ts +2 -5
- package/types/handler/plsql/procedureVariable.d.ts +1 -1
- package/types/handler/plsql/request.d.ts +3 -1
- package/types/handler/plsql/sendResponse.d.ts +1 -1
- package/types/server/server.d.ts +22 -0
- package/types/types.d.ts +18 -0
- package/types/util/cache.d.ts +69 -0
- package/types/util/jsonLogger.d.ts +45 -0
|
@@ -15,6 +15,7 @@ import {getProcedureNamed} from './procedureNamed.js';
|
|
|
15
15
|
import {parsePage} from './parsePage.js';
|
|
16
16
|
import {sendResponse} from './sendResponse.js';
|
|
17
17
|
import {ProcedureError} from './procedureError.js';
|
|
18
|
+
import {RequestError} from './requestError.js';
|
|
18
19
|
import {inspect, getBlock} from '../../util/trace.js';
|
|
19
20
|
import {errorToString} from '../../util/errorToString.js';
|
|
20
21
|
import {sanitizeProcName} from './procedureSanitize.js';
|
|
@@ -29,6 +30,8 @@ import {sanitizeProcName} from './procedureSanitize.js';
|
|
|
29
30
|
* @typedef {import('../../types.js').environmentType} environmentType
|
|
30
31
|
* @typedef {import('../../types.js').configPlSqlHandlerType} configPlSqlHandlerType
|
|
31
32
|
* @typedef {import('../../types.js').BindParameterConfig} BindParameterConfig
|
|
33
|
+
* @typedef {import('../../util/cache.js').Cache<string>} ProcedureNameCache
|
|
34
|
+
* @typedef {import('../../util/cache.js').Cache<import('./procedureNamed.js').argsType>} ArgumentCache
|
|
32
35
|
*/
|
|
33
36
|
|
|
34
37
|
/**
|
|
@@ -38,9 +41,11 @@ import {sanitizeProcName} from './procedureSanitize.js';
|
|
|
38
41
|
* @param {argObjType} argObj - The arguments to pass to the procedure
|
|
39
42
|
* @param {configPlSqlHandlerType} options - The options for the middleware
|
|
40
43
|
* @param {Connection} databaseConnection - The database connection
|
|
41
|
-
* @
|
|
44
|
+
* @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
|
|
45
|
+
* @param {ArgumentCache} argumentCache - The argument cache.
|
|
46
|
+
* @returns {Promise<{sql: string; bind: BindParameterConfig; resolvedName?: string}>} - The SQL statement and bindings for the procedure to execute
|
|
42
47
|
*/
|
|
43
|
-
const getProcedure = async (req, procName, argObj, options, databaseConnection) => {
|
|
48
|
+
const getProcedure = async (req, procName, argObj, options, databaseConnection, procedureNameCache, argumentCache) => {
|
|
44
49
|
// path alias
|
|
45
50
|
if (options.pathAlias?.toLowerCase() === procName.toLowerCase()) {
|
|
46
51
|
debug(`getProcedure: path alias "${options.pathAlias}" redirects to "${options.pathAliasProcedure}"`);
|
|
@@ -56,13 +61,20 @@ const getProcedure = async (req, procName, argObj, options, databaseConnection)
|
|
|
56
61
|
const useVariableArguments = procName.startsWith('!');
|
|
57
62
|
|
|
58
63
|
// sanitize procedure name
|
|
59
|
-
const
|
|
64
|
+
const rawName = useVariableArguments ? procName.substring(1) : procName;
|
|
65
|
+
const sanitizedProcName = await sanitizeProcName(rawName, databaseConnection, options, procedureNameCache);
|
|
60
66
|
|
|
61
67
|
// run procedure
|
|
62
68
|
if (useVariableArguments) {
|
|
63
|
-
return
|
|
69
|
+
return {
|
|
70
|
+
...getProcedureVariable(req, sanitizedProcName, argObj),
|
|
71
|
+
resolvedName: sanitizedProcName,
|
|
72
|
+
};
|
|
64
73
|
} else {
|
|
65
|
-
return
|
|
74
|
+
return {
|
|
75
|
+
...(await getProcedureNamed(req, sanitizedProcName, argObj, databaseConnection, argumentCache)),
|
|
76
|
+
resolvedName: sanitizedProcName,
|
|
77
|
+
};
|
|
66
78
|
}
|
|
67
79
|
};
|
|
68
80
|
|
|
@@ -122,11 +134,11 @@ const procedureExecute = async (para, databaseConnection) => {
|
|
|
122
134
|
/**
|
|
123
135
|
* Get page from procedure
|
|
124
136
|
*
|
|
125
|
-
* @param {boolean}
|
|
137
|
+
* @param {boolean} _test - Test.
|
|
126
138
|
* @param {Connection} databaseConnection - Database connection.
|
|
127
139
|
* @returns {Promise<string>} Promise resolving to the returned page content.
|
|
128
140
|
*/
|
|
129
|
-
const procedureGetPage = async (
|
|
141
|
+
const procedureGetPage = async (_test, databaseConnection) => {
|
|
130
142
|
const MAX_IROWS = 100000;
|
|
131
143
|
|
|
132
144
|
/** @type {BindParameterConfig} */
|
|
@@ -229,9 +241,11 @@ END;
|
|
|
229
241
|
* @param {fileUploadType[]} filesToUpload - Array of files to be uploaded
|
|
230
242
|
* @param {configPlSqlHandlerType} options - the options for the middleware.
|
|
231
243
|
* @param {Connection} databaseConnection - Database connection.
|
|
244
|
+
* @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
|
|
245
|
+
* @param {ArgumentCache} argumentCache - The argument cache.
|
|
232
246
|
* @returns {Promise<void>} Promise resolving to the page content generated by the executed procedure
|
|
233
247
|
*/
|
|
234
|
-
export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options, databaseConnection) => {
|
|
248
|
+
export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options, databaseConnection, procedureNameCache, argumentCache) => {
|
|
235
249
|
debug('invokeProcedure: begin');
|
|
236
250
|
|
|
237
251
|
// 1) upload files
|
|
@@ -249,7 +263,12 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
|
|
|
249
263
|
// 2) get procedure to execute and the arguments
|
|
250
264
|
|
|
251
265
|
debug('invokeProcedure: get procedure to execute and the arguments');
|
|
252
|
-
|
|
266
|
+
// Extract the raw procedure name from params
|
|
267
|
+
const rawProcName = Array.isArray(req.params.name) ? req.params.name[0] : req.params.name;
|
|
268
|
+
if (!rawProcName) {
|
|
269
|
+
throw new RequestError('No procedure name provided');
|
|
270
|
+
}
|
|
271
|
+
const para = await getProcedure(req, rawProcName, argObj, options, databaseConnection, procedureNameCache, argumentCache);
|
|
253
272
|
|
|
254
273
|
// 3) prepare the session
|
|
255
274
|
|
|
@@ -259,7 +278,34 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
|
|
|
259
278
|
// 4) execute the procedure
|
|
260
279
|
|
|
261
280
|
debug('invokeProcedure: execute the session');
|
|
262
|
-
|
|
281
|
+
try {
|
|
282
|
+
await procedureExecute(para, databaseConnection);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
// Invalidation Logic
|
|
285
|
+
if (err instanceof ProcedureError) {
|
|
286
|
+
const errorString = err.toString();
|
|
287
|
+
// Check for ORA-04068, ORA-04061, ORA-04065, ORA-06550
|
|
288
|
+
if (
|
|
289
|
+
errorString.includes('ORA-04068') ||
|
|
290
|
+
errorString.includes('ORA-04061') ||
|
|
291
|
+
errorString.includes('ORA-04065') ||
|
|
292
|
+
errorString.includes('ORA-06550')
|
|
293
|
+
) {
|
|
294
|
+
debug(`invokeProcedure: detected invalidation error (${errorString}). Clearing caches.`);
|
|
295
|
+
|
|
296
|
+
// Clear name resolution cache for the input name
|
|
297
|
+
if (rawProcName) {
|
|
298
|
+
procedureNameCache.delete(rawProcName);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Clear argument cache for the resolved name
|
|
302
|
+
if (para.resolvedName) {
|
|
303
|
+
argumentCache.delete(para.resolvedName.toUpperCase());
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
263
309
|
|
|
264
310
|
// 5) get the page returned from the procedure
|
|
265
311
|
|
|
@@ -286,7 +332,7 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
|
|
|
286
332
|
const pageComponents = parsePage(lines);
|
|
287
333
|
|
|
288
334
|
// add "Server" header
|
|
289
|
-
pageComponents.head.server = cgiObj.SERVER_SOFTWARE;
|
|
335
|
+
pageComponents.head.server = cgiObj.SERVER_SOFTWARE ?? '';
|
|
290
336
|
|
|
291
337
|
// add file download information
|
|
292
338
|
if (fileDownload.fileType !== '' && fileDownload.fileSize > 0 && fileDownload.fileBlob !== null) {
|
|
@@ -21,7 +21,7 @@ import {toTable, warningMessage} from '../../util/trace.js';
|
|
|
21
21
|
* @typedef {import('../../types.js').BindParameterConfig} BindParameterConfig
|
|
22
22
|
* @typedef {import('../../types.js').BindParameter} BindParameter
|
|
23
23
|
* @typedef {Record<string, string>} argsType
|
|
24
|
-
* @typedef {
|
|
24
|
+
* @typedef {import('../../util/cache.js').Cache<argsType>} ArgumentCache
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
const SQL_GET_ARGUMENT = [
|
|
@@ -60,11 +60,6 @@ const DATA_TYPES = Object.freeze({
|
|
|
60
60
|
// REF CURSOR
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
// NOTE: Consider using a separate cache for each database pool to avoid possible conflicts.
|
|
64
|
-
/** @type {Map<string, cacheEntryType>} */
|
|
65
|
-
const ARGS_CACHE = new Map();
|
|
66
|
-
const ARGS_CACHE_MAX_COUNT = 10000;
|
|
67
|
-
|
|
68
63
|
/**
|
|
69
64
|
* Retrieve the argument types for a given procedure to be executed.
|
|
70
65
|
* This is important because if the procedure is defined to take a PL/SQL indexed table,
|
|
@@ -115,61 +110,36 @@ const loadArguments = async (procedure, databaseConnection) => {
|
|
|
115
110
|
/** @type {Record<string, string>} */
|
|
116
111
|
const argTypes = {};
|
|
117
112
|
for (let i = 0; i < data.names.length; i++) {
|
|
118
|
-
|
|
113
|
+
const name = data.names[i];
|
|
114
|
+
const type = data.types[i];
|
|
115
|
+
if (name && type) {
|
|
116
|
+
argTypes[name.toLowerCase()] = type;
|
|
117
|
+
}
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
return argTypes;
|
|
122
121
|
};
|
|
123
122
|
|
|
124
|
-
/**
|
|
125
|
-
* Remove the cache entries with the lowest hitCount.
|
|
126
|
-
* @param {number} count - Number of entries to remove
|
|
127
|
-
* @returns {void}
|
|
128
|
-
*/
|
|
129
|
-
const removeLowestHitCountEntries = (count) => {
|
|
130
|
-
// Convert cache entries to an array
|
|
131
|
-
const entries = Array.from(ARGS_CACHE.entries());
|
|
132
|
-
|
|
133
|
-
// Sort entries by hitCount in ascending order
|
|
134
|
-
entries.sort((a, b) => a[1].hitCount - b[1].hitCount);
|
|
135
|
-
|
|
136
|
-
// Get the keys of the `count` entries with the lowest hitCount
|
|
137
|
-
const keysToRemove = entries.slice(0, count).map(([key]) => key);
|
|
138
|
-
|
|
139
|
-
// Remove these entries from the cache
|
|
140
|
-
for (const key of keysToRemove) {
|
|
141
|
-
ARGS_CACHE.delete(key);
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
123
|
/**
|
|
146
124
|
* Find the argument types for a given procedure to be executed.
|
|
147
125
|
* As the arguments are cached, we first look up the cache and only if not yet available we load them.
|
|
148
126
|
* @param {string} procedure - The procedure
|
|
149
127
|
* @param {Connection} databaseConnection - The database connection
|
|
128
|
+
* @param {ArgumentCache} argumentCache - The argument cache.
|
|
150
129
|
* @returns {Promise<argsType>} - The argument types
|
|
151
130
|
*/
|
|
152
|
-
const findArguments = async (procedure, databaseConnection) => {
|
|
131
|
+
const findArguments = async (procedure, databaseConnection, argumentCache) => {
|
|
153
132
|
const key = procedure.toUpperCase();
|
|
154
133
|
|
|
155
134
|
// lookup in the cache
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
// if we fount the procedure in the cache, we increase the hit cound and return
|
|
159
|
-
if (cacheEntry) {
|
|
160
|
-
cacheEntry.hitCount++;
|
|
161
|
-
if (debug.enabled) {
|
|
162
|
-
debug(`findArguments: procedure "${procedure}" found in cache with "${cacheEntry.hitCount}" hits`);
|
|
163
|
-
}
|
|
164
|
-
return cacheEntry.args;
|
|
165
|
-
}
|
|
135
|
+
const cachedArgs = argumentCache.get(key);
|
|
166
136
|
|
|
167
|
-
// if
|
|
168
|
-
if (
|
|
137
|
+
// if we found the procedure in the cache, we return it
|
|
138
|
+
if (cachedArgs) {
|
|
169
139
|
if (debug.enabled) {
|
|
170
|
-
debug(`findArguments:
|
|
140
|
+
debug(`findArguments: procedure "${procedure}" found in cache`);
|
|
171
141
|
}
|
|
172
|
-
|
|
142
|
+
return cachedArgs;
|
|
173
143
|
}
|
|
174
144
|
|
|
175
145
|
// load from database
|
|
@@ -179,7 +149,7 @@ const findArguments = async (procedure, databaseConnection) => {
|
|
|
179
149
|
const args = await loadArguments(procedure, databaseConnection);
|
|
180
150
|
|
|
181
151
|
// add to the cache
|
|
182
|
-
|
|
152
|
+
argumentCache.set(key, args);
|
|
183
153
|
|
|
184
154
|
return args;
|
|
185
155
|
};
|
|
@@ -238,7 +208,7 @@ export const getBinding = (argName, argValue, argType) => {
|
|
|
238
208
|
*/
|
|
239
209
|
const inspectBindings = (argObj, argTypes) => {
|
|
240
210
|
const rows = Object.entries(argObj).map(([key, value]) => {
|
|
241
|
-
return [key, value.toString(), typeof value, argTypes[key.toLowerCase()]];
|
|
211
|
+
return [key, value.toString(), typeof value, argTypes[key.toLowerCase()] ?? 'unknown'];
|
|
242
212
|
});
|
|
243
213
|
|
|
244
214
|
const {text} = toTable(['id', 'value', 'value type', 'argument type'], rows);
|
|
@@ -252,13 +222,14 @@ const inspectBindings = (argObj, argTypes) => {
|
|
|
252
222
|
* @param {string} procName - The procedure to execute
|
|
253
223
|
* @param {argObjType} argObj - The arguments to pass to the procedure
|
|
254
224
|
* @param {Connection} databaseConnection - The database connection
|
|
225
|
+
* @param {ArgumentCache} argumentCache - The argument cache.
|
|
255
226
|
* @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
|
|
256
227
|
*/
|
|
257
|
-
export const getProcedureNamed = async (req, procName, argObj, databaseConnection) => {
|
|
228
|
+
export const getProcedureNamed = async (req, procName, argObj, databaseConnection, argumentCache) => {
|
|
258
229
|
debug(`getProcedureNamed: ${procName} arguments=`, argObj);
|
|
259
230
|
|
|
260
231
|
// get the types of the arguments
|
|
261
|
-
const argTypes = await findArguments(procName, databaseConnection);
|
|
232
|
+
const argTypes = await findArguments(procName, databaseConnection, argumentCache);
|
|
262
233
|
|
|
263
234
|
/** @type {string[]} */
|
|
264
235
|
const sqlParameter = [];
|
|
@@ -16,17 +16,90 @@ import {errorToString} from '../../util/errorToString.js';
|
|
|
16
16
|
* @typedef {import('../../types.js').environmentType} environmentType
|
|
17
17
|
* @typedef {import('../../types.js').configPlSqlHandlerType} configPlSqlHandlerType
|
|
18
18
|
* @typedef {import('../../types.js').BindParameterConfig} BindParameterConfig
|
|
19
|
+
* @typedef {import('../../util/cache.js').Cache<string>} ProcedureNameCache
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
const DEFAULT_EXCLUSION_LIST = ['sys.', 'dbms_', 'utl_', 'owa_', 'htp.', 'htf.', 'wpg_docload.', 'ctxsys.', 'mdsys.'];
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
import {Cache} from '../../util/cache.js';
|
|
25
|
+
/** @type {Cache<{valid: boolean}>} */
|
|
26
|
+
const validationFunctionCache = new Cache();
|
|
27
|
+
|
|
24
28
|
/**
|
|
25
|
-
*
|
|
29
|
+
* Resolve the procedure name using dbms_utility.name_resolve.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} procName - The procedure name to resolve.
|
|
32
|
+
* @param {Connection} databaseConnection - The database connection.
|
|
33
|
+
* @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
|
|
34
|
+
* @returns {Promise<string>} - The resolved canonical procedure name (SCHEMA.NAME).
|
|
26
35
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
36
|
+
const resolveProcedureName = async (procName, databaseConnection, procedureNameCache) => {
|
|
37
|
+
// Check cache
|
|
38
|
+
const cachedName = procedureNameCache.get(procName);
|
|
39
|
+
if (cachedName) {
|
|
40
|
+
debug(`resolveProcedureName: Cache hit for "${procName}" -> "${cachedName}"`);
|
|
41
|
+
return cachedName;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
debug(`resolveProcedureName: Cache miss for "${procName}". Resolving in DB...`);
|
|
45
|
+
|
|
46
|
+
const sql = `
|
|
47
|
+
DECLARE
|
|
48
|
+
l_schema VARCHAR2(128);
|
|
49
|
+
l_part1 VARCHAR2(128);
|
|
50
|
+
l_part2 VARCHAR2(128);
|
|
51
|
+
l_dblink VARCHAR2(128);
|
|
52
|
+
l_part1_type NUMBER;
|
|
53
|
+
l_object_number NUMBER;
|
|
54
|
+
BEGIN
|
|
55
|
+
dbms_utility.name_resolve(
|
|
56
|
+
name => :name,
|
|
57
|
+
context => 1,
|
|
58
|
+
schema => l_schema,
|
|
59
|
+
part1 => l_part1,
|
|
60
|
+
part2 => l_part2,
|
|
61
|
+
dblink => l_dblink,
|
|
62
|
+
part1_type => l_part1_type,
|
|
63
|
+
object_number => l_object_number
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
-- Reconstruct the canonical name
|
|
67
|
+
-- If it's a package procedure: schema.package.procedure
|
|
68
|
+
-- If it's a standalone procedure: schema.procedure
|
|
69
|
+
|
|
70
|
+
IF l_part1 IS NOT NULL THEN
|
|
71
|
+
:resolved := l_schema || '.' || l_part1 || '.' || l_part2;
|
|
72
|
+
ELSE
|
|
73
|
+
:resolved := l_schema || '.' || l_part2;
|
|
74
|
+
END IF;
|
|
75
|
+
END;
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
const bind = {
|
|
79
|
+
name: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: procName},
|
|
80
|
+
resolved: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxSize: 400},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const result = await databaseConnection.execute(sql, bind);
|
|
85
|
+
const {resolved} = z.object({resolved: z.string()}).parse(result.outBinds);
|
|
86
|
+
|
|
87
|
+
if (!resolved) {
|
|
88
|
+
throw new RequestError(`Could not resolve procedure name "${procName}"`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
debug(`resolveProcedureName: Resolved "${procName}" -> "${resolved}"`);
|
|
92
|
+
|
|
93
|
+
// Update cache
|
|
94
|
+
procedureNameCache.set(procName, resolved);
|
|
95
|
+
|
|
96
|
+
return resolved;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
debug(`resolveProcedureName: Error resolving "${procName}"`, err);
|
|
99
|
+
// Rethrow as RequestError to indicate 404/403
|
|
100
|
+
throw new RequestError(`Procedure "${procName}" not found or not accessible.\n${errorToString(err)}`);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
30
103
|
|
|
31
104
|
/**
|
|
32
105
|
* Sanitize the procedure name.
|
|
@@ -34,15 +107,16 @@ const REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT = 10000;
|
|
|
34
107
|
* @param {string} procName - The procedure name.
|
|
35
108
|
* @param {Connection} databaseConnection - The database connection
|
|
36
109
|
* @param {configPlSqlHandlerType} options - the options for the middleware.
|
|
110
|
+
* @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
|
|
37
111
|
* @returns {Promise<string>} Promise resolving to final procedure name.
|
|
38
112
|
*/
|
|
39
|
-
export const sanitizeProcName = async (procName, databaseConnection, options) => {
|
|
113
|
+
export const sanitizeProcName = async (procName, databaseConnection, options, procedureNameCache) => {
|
|
40
114
|
debug('sanitizeProcName', procName);
|
|
41
115
|
|
|
42
116
|
// make lowercase and trim
|
|
43
117
|
let finalProcName = procName.toLowerCase().trim();
|
|
44
118
|
|
|
45
|
-
// remove special characters
|
|
119
|
+
// remove special characters (basic sanity check before DB call)
|
|
46
120
|
finalProcName = removeSpecialCharacters(finalProcName);
|
|
47
121
|
|
|
48
122
|
// check for default exclusions
|
|
@@ -67,6 +141,10 @@ export const sanitizeProcName = async (procName, databaseConnection, options) =>
|
|
|
67
141
|
|
|
68
142
|
// Check request validation function
|
|
69
143
|
if (options.requestValidationFunction && options.requestValidationFunction.length > 0) {
|
|
144
|
+
// Note: We might want to scope this cache too, but for now let's focus on the procedure name cache.
|
|
145
|
+
// The original code used a global cache for this.
|
|
146
|
+
// For strict correctness, we should probably verify the *resolved* name,
|
|
147
|
+
// but legacy behavior checks the input name.
|
|
70
148
|
const valid = await requestValidationFunction(finalProcName, options.requestValidationFunction, databaseConnection);
|
|
71
149
|
if (!valid) {
|
|
72
150
|
const error = `Procedure name "${procName}" is not valid according to the request validation function "${options.requestValidationFunction}"`;
|
|
@@ -75,7 +153,11 @@ export const sanitizeProcName = async (procName, databaseConnection, options) =>
|
|
|
75
153
|
}
|
|
76
154
|
}
|
|
77
155
|
|
|
78
|
-
|
|
156
|
+
// NEW: Resolve the procedure name against the database
|
|
157
|
+
// This prevents SQL injection and ensures the procedure exists
|
|
158
|
+
const resolvedName = await resolveProcedureName(finalProcName, databaseConnection, procedureNameCache);
|
|
159
|
+
|
|
160
|
+
return resolvedName;
|
|
79
161
|
};
|
|
80
162
|
|
|
81
163
|
/**
|
|
@@ -142,27 +224,6 @@ const loadRequestValid = async (procName, requestValidationFunction, databaseCon
|
|
|
142
224
|
}
|
|
143
225
|
};
|
|
144
226
|
|
|
145
|
-
/**
|
|
146
|
-
* Remove the cache entries with the lowest hitCount.
|
|
147
|
-
* @param {number} count - Number of entries to remove
|
|
148
|
-
* @returns {void}
|
|
149
|
-
*/
|
|
150
|
-
const removeLowestHitCountEntries = (count) => {
|
|
151
|
-
// Convert cache entries to an array
|
|
152
|
-
const entries = Array.from(REQUEST_VALIDATION_FUNCTION_CACHE.entries());
|
|
153
|
-
|
|
154
|
-
// Sort entries by hitCount in ascending order
|
|
155
|
-
entries.sort((a, b) => a[1].hitCount - b[1].hitCount);
|
|
156
|
-
|
|
157
|
-
// Get the keys of the `count` entries with the lowest hitCount
|
|
158
|
-
const keysToRemove = entries.slice(0, count).map(([key]) => key);
|
|
159
|
-
|
|
160
|
-
// Remove these entries from the cache
|
|
161
|
-
for (const key of keysToRemove) {
|
|
162
|
-
REQUEST_VALIDATION_FUNCTION_CACHE.delete(key);
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
227
|
/**
|
|
167
228
|
* Request validation function.
|
|
168
229
|
*
|
|
@@ -175,37 +236,27 @@ const requestValidationFunction = async (procName, requestValidationFunction, da
|
|
|
175
236
|
debug('requestValidationFunction', procName, requestValidationFunction);
|
|
176
237
|
|
|
177
238
|
// calculate the key
|
|
178
|
-
//const key = `${databaseConnection.connectString}_${databaseConnection.user}_${procName.toLowerCase()}`;
|
|
179
239
|
const key = procName.toLowerCase();
|
|
180
240
|
|
|
181
241
|
// lookup in the cache
|
|
182
|
-
const cacheEntry =
|
|
242
|
+
const cacheEntry = validationFunctionCache.get(key);
|
|
183
243
|
|
|
184
|
-
// if we
|
|
185
|
-
if (cacheEntry) {
|
|
186
|
-
cacheEntry.hitCount++;
|
|
244
|
+
// if we found the procedure in the cache, we return it (hitCount already incremented by get)
|
|
245
|
+
if (cacheEntry !== undefined) {
|
|
187
246
|
if (debug.enabled) {
|
|
188
|
-
debug(`
|
|
247
|
+
debug(`requestValidationFunction: procedure "${procName}" found in cache`);
|
|
189
248
|
}
|
|
190
249
|
return cacheEntry.valid;
|
|
191
250
|
}
|
|
192
251
|
|
|
193
|
-
// if the cache is full, we remove the 1000 least used cache entries
|
|
194
|
-
if (REQUEST_VALIDATION_FUNCTION_CACHE.size > REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT) {
|
|
195
|
-
if (debug.enabled) {
|
|
196
|
-
debug(`findArguments: cache is full. size=${REQUEST_VALIDATION_FUNCTION_CACHE.size} max=${REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT}`);
|
|
197
|
-
}
|
|
198
|
-
removeLowestHitCountEntries(1000);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
252
|
// load from database
|
|
202
253
|
if (debug.enabled) {
|
|
203
|
-
debug(`
|
|
254
|
+
debug(`requestValidationFunction: procedure "${procName}" not found in cache and must be loaded`);
|
|
204
255
|
}
|
|
205
256
|
const valid = await loadRequestValid(procName, requestValidationFunction, databaseConnection);
|
|
206
257
|
|
|
207
258
|
// add to the cache
|
|
208
|
-
|
|
259
|
+
validationFunctionCache.set(key, {valid});
|
|
209
260
|
|
|
210
261
|
return valid;
|
|
211
262
|
};
|
|
@@ -18,12 +18,12 @@ import oracledb from 'oracledb';
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Get the sql statement and bindings for the procedure to execute for a variable number of arguments
|
|
21
|
-
* @param {Request}
|
|
21
|
+
* @param {Request} _req - The req object represents the HTTP request. (only used for debugging)
|
|
22
22
|
* @param {string} procName - The procedure to execute
|
|
23
23
|
* @param {argObjType} argObj - The arguments to pass to the procedure
|
|
24
24
|
* @returns {{sql: string; bind: BindParameterConfig}} - The SQL statement and bindings for the procedure to execute
|
|
25
25
|
*/
|
|
26
|
-
export const getProcedureVariable = (
|
|
26
|
+
export const getProcedureVariable = (_req, procName, argObj) => {
|
|
27
27
|
if (debug.enabled) {
|
|
28
28
|
debug(`getProcedureVariable: ${procName} arguments=`, argObj);
|
|
29
29
|
}
|
|
@@ -19,6 +19,8 @@ import {isStringOrArrayOfString} from '../../util/type.js';
|
|
|
19
19
|
* @typedef {import('oracledb').Connection} Connection
|
|
20
20
|
* @typedef {import('../../types.js').argObjType} argObjType
|
|
21
21
|
* @typedef {import('../../types.js').configPlSqlHandlerType} configPlSqlHandlerType
|
|
22
|
+
* @typedef {import('../../util/cache.js').Cache<string>} ProcedureNameCache
|
|
23
|
+
* @typedef {import('../../util/cache.js').Cache<import('./procedureNamed.js').argsType>} ArgumentCache
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -28,9 +30,11 @@ import {isStringOrArrayOfString} from '../../util/type.js';
|
|
|
28
30
|
* @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
|
|
29
31
|
* @param {configPlSqlHandlerType} options - the options for the middleware.
|
|
30
32
|
* @param {Pool} connectionPool - The connection pool.
|
|
33
|
+
* @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
|
|
34
|
+
* @param {ArgumentCache} argumentCache - The argument cache.
|
|
31
35
|
* @returns {Promise<void>} - Promise resolving to th page
|
|
32
36
|
*/
|
|
33
|
-
export const processRequest = async (req, res, options, connectionPool) => {
|
|
37
|
+
export const processRequest = async (req, res, options, connectionPool, procedureNameCache, argumentCache) => {
|
|
34
38
|
debug('processRequest: ENTER');
|
|
35
39
|
|
|
36
40
|
//
|
|
@@ -65,7 +69,7 @@ export const processRequest = async (req, res, options, connectionPool) => {
|
|
|
65
69
|
debug('processRequest: argObj=', argObj);
|
|
66
70
|
|
|
67
71
|
// invoke the Oracle procedure and get the page contenst
|
|
68
|
-
await invokeProcedure(req, res, argObj, cgiObj, filesToUpload, options, connection);
|
|
72
|
+
await invokeProcedure(req, res, argObj, cgiObj, filesToUpload, options, connection, procedureNameCache, argumentCache);
|
|
69
73
|
|
|
70
74
|
// transaction mode
|
|
71
75
|
if (options.transactionMode === 'rollback') {
|
|
@@ -73,7 +77,8 @@ export const processRequest = async (req, res, options, connectionPool) => {
|
|
|
73
77
|
await connection.rollback();
|
|
74
78
|
} else if (typeof options.transactionMode === 'function') {
|
|
75
79
|
debug('transactionMode: callback');
|
|
76
|
-
const
|
|
80
|
+
const procName = Array.isArray(req.params.name) ? req.params.name[0] : req.params.name;
|
|
81
|
+
const result = options.transactionMode(connection, procName ?? '');
|
|
77
82
|
debug('transactionMode: callback restult', result);
|
|
78
83
|
if (result && typeof result.then === 'function') {
|
|
79
84
|
await result;
|
|
@@ -18,12 +18,12 @@ import {getBlock} from '../../util/trace.js';
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Send "default" response to the browser
|
|
21
|
-
* @param {Request}
|
|
21
|
+
* @param {Request} _req - The req object represents the HTTP request.
|
|
22
22
|
* @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
|
|
23
23
|
* @param {pageType} page - The page to send.
|
|
24
24
|
* @returns {Promise<void>}
|
|
25
25
|
*/
|
|
26
|
-
export const sendResponse = async (
|
|
26
|
+
export const sendResponse = async (_req, res, page) => {
|
|
27
27
|
/** @type {string[]} */
|
|
28
28
|
const debugText = [];
|
|
29
29
|
|
package/src/server/config.js
CHANGED
|
@@ -17,6 +17,7 @@ export const showConfig = (config) => {
|
|
|
17
17
|
console.log(LINE);
|
|
18
18
|
|
|
19
19
|
console.log(`Server port: ${config.port}`);
|
|
20
|
+
console.log(`Admin route: ${config.adminRoute ?? '/admin'}${config.adminUser ? ' (authenticated)' : ''}`);
|
|
20
21
|
console.log(`Access log: ${config.loggerFilename.length > 0 ? config.loggerFilename : ''}`);
|
|
21
22
|
console.log(`Upload file size limit: ${typeof config.uploadFileSizeLimit === 'number' ? `${config.uploadFileSizeLimit} bytes` : 'any'}`);
|
|
22
23
|
|