@contrail/flexplm 1.7.7-alpha.d153eea → 1.7.7-alpha.e0ee6a0
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/lib/interfaces/interfaces.d.ts +13 -5
- package/lib/util/flexplm-connect.d.ts +1 -16
- package/lib/util/flexplm-connect.js +44 -197
- package/lib/util/flexplm-connect.spec.js +91 -6
- package/lib/util/flexplm-session-manager.d.ts +40 -0
- package/lib/util/flexplm-session-manager.js +180 -0
- package/lib/util/flexplm-session-manager.spec.d.ts +1 -0
- package/lib/util/flexplm-session-manager.spec.js +53 -0
- package/package.json +1 -1
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
export interface SessionReuseConfig {
|
|
2
|
+
enabled?: boolean | string;
|
|
3
|
+
ttlSeconds?: number | string;
|
|
4
|
+
cookieName?: string;
|
|
5
|
+
retryStatuses?: number | number[] | string;
|
|
6
|
+
}
|
|
7
|
+
export interface FlexPLMConnectConfig {
|
|
8
|
+
staticHeaders?: {
|
|
9
|
+
[key: string]: string;
|
|
10
|
+
};
|
|
11
|
+
sessionReuse?: SessionReuseConfig;
|
|
12
|
+
}
|
|
1
13
|
export interface FCConfig {
|
|
2
14
|
apiKey?: string;
|
|
3
15
|
orgSlug?: string;
|
|
@@ -5,11 +17,7 @@ export interface FCConfig {
|
|
|
5
17
|
configFile?: string;
|
|
6
18
|
eventObjectClassParam?: string;
|
|
7
19
|
eventFlexTypePathParam?: string;
|
|
8
|
-
flexplmConnect?:
|
|
9
|
-
'flexplmConnect.sessionReuse.enabled'?: boolean | string;
|
|
10
|
-
'flexplmConnect.sessionReuse.ttlSeconds'?: number | string;
|
|
11
|
-
'flexplmConnect.sessionReuse.cookieName'?: string;
|
|
12
|
-
'flexplmConnect.sessionReuse.retryStatuses'?: number | number[] | string;
|
|
20
|
+
flexplmConnect?: FlexPLMConnectConfig;
|
|
13
21
|
taskId?: string;
|
|
14
22
|
apiHost: string;
|
|
15
23
|
userName(): string;
|
|
@@ -1,28 +1,13 @@
|
|
|
1
1
|
import { FCConfig, FlexPLMResponseData, PayloadType } from '../interfaces/interfaces';
|
|
2
2
|
export declare class FlexPLMConnect {
|
|
3
3
|
private config;
|
|
4
|
+
private sessions;
|
|
4
5
|
private vibeEventEndpoint;
|
|
5
6
|
private staticHeaders;
|
|
6
7
|
payloadSendAsArray: boolean;
|
|
7
8
|
constructor(_config: FCConfig, endPoint?: any, payloadAsArray?: any);
|
|
8
|
-
private get sessionOptions();
|
|
9
|
-
/**
|
|
10
|
-
* Waits until no cold session acquisition is running for this key.
|
|
11
|
-
* Callers must `await` this only when it returns a promise: awaiting even a resolved promise yields,
|
|
12
|
-
* and the check-then-register in runColdAcquisition has to stay synchronous to be race free.
|
|
13
|
-
*/
|
|
14
|
-
private waitForInFlightAcquisition;
|
|
15
|
-
/** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
|
|
16
|
-
private runColdAcquisition;
|
|
17
|
-
private getSessionKey;
|
|
18
9
|
/** Releases the connection of a rejected response before it is replaced by a retry. */
|
|
19
10
|
private discardBody;
|
|
20
|
-
private getCachedSession;
|
|
21
|
-
private storeSession;
|
|
22
|
-
private clearSession;
|
|
23
|
-
/** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
|
|
24
|
-
private extractSessionCookie;
|
|
25
|
-
private cookieHeader;
|
|
26
11
|
private getRequestOptions;
|
|
27
12
|
getCSRF(getNewSession?: boolean, rejectedCookie?: string): any;
|
|
28
13
|
private fetchCSRF;
|
|
@@ -2,62 +2,23 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FlexPLMConnect = void 0;
|
|
4
4
|
const app_framework_1 = require("@contrail/app-framework");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
sessionCache.delete(key);
|
|
5
|
+
const flexplm_session_manager_1 = require("./flexplm-session-manager");
|
|
6
|
+
// Cookie values carry the session id: mask them in logs whatever the header-name casing.
|
|
7
|
+
const maskCookieHeaders = (headers) => {
|
|
8
|
+
if (!headers)
|
|
9
|
+
return;
|
|
10
|
+
for (const key of Object.keys(headers)) {
|
|
11
|
+
if (key.toLowerCase() === 'cookie' && headers[key])
|
|
12
|
+
headers[key] = '***masked***';
|
|
14
13
|
}
|
|
15
|
-
// Still full: drop the oldest entries (Map keeps insertion order).
|
|
16
|
-
while (sessionCache.size >= MAX_CACHED_SESSIONS) {
|
|
17
|
-
sessionCache.delete(sessionCache.keys().next().value);
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
// In-flight cold session acquisitions per key, so concurrent events share one FlexPLM session instead of each creating their own.
|
|
21
|
-
const inFlightAcquisitions = new Map();
|
|
22
|
-
const DEFAULT_SESSION_COOKIE_NAME = 'JSESSIONID';
|
|
23
|
-
const DEFAULT_SESSION_TTL_SECONDS = 900;
|
|
24
|
-
const DEFAULT_RETRY_STATUSES = [401, 403];
|
|
25
|
-
const COOKIE_NAME_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
26
|
-
// Thrown internally when a cached cookie was rejected, so the caller can re-acquire under the lock.
|
|
27
|
-
class SessionRejectedError extends Error {
|
|
28
|
-
}
|
|
29
|
-
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
|
-
const parseRetryStatuses = (value) => {
|
|
31
|
-
let list = [];
|
|
32
|
-
if (Array.isArray(value))
|
|
33
|
-
list = value;
|
|
34
|
-
else if (typeof value === 'string')
|
|
35
|
-
list = value.split(',');
|
|
36
|
-
else if (value !== undefined && value !== null)
|
|
37
|
-
list = [value];
|
|
38
|
-
const statuses = list
|
|
39
|
-
.map(status => (typeof status === 'string' ? status.trim() : status))
|
|
40
|
-
.filter(status => status !== '' && status !== null && status !== undefined)
|
|
41
|
-
.map(Number)
|
|
42
|
-
// Only client-error statuses can mean "session rejected". 2xx/5xx would resend events the server already processed.
|
|
43
|
-
.filter(status => Number.isInteger(status) && status >= 400 && status <= 499);
|
|
44
|
-
return statuses.length ? statuses : DEFAULT_RETRY_STATUSES;
|
|
45
|
-
};
|
|
46
|
-
const parseTtlSeconds = (value) => {
|
|
47
|
-
const ttl = Number(value);
|
|
48
|
-
return Number.isFinite(ttl) && ttl > 0 ? ttl : DEFAULT_SESSION_TTL_SECONDS;
|
|
49
14
|
};
|
|
50
|
-
const parseCookieName = (value) => {
|
|
51
|
-
const name = typeof value === 'string' ? value.trim() : '';
|
|
52
|
-
return COOKIE_NAME_TOKEN.test(name) ? name : DEFAULT_SESSION_COOKIE_NAME;
|
|
53
|
-
};
|
|
54
|
-
// Added for Session reuse - ET-390 - Ends
|
|
55
15
|
class FlexPLMConnect {
|
|
56
16
|
constructor(_config, endPoint = undefined, payloadAsArray = undefined) {
|
|
57
17
|
this.vibeEventEndpoint = '';
|
|
58
18
|
this.staticHeaders = undefined;
|
|
59
19
|
this.payloadSendAsArray = true;
|
|
60
20
|
this.config = _config;
|
|
21
|
+
this.sessions = new flexplm_session_manager_1.FlexPLMSessionManager(_config);
|
|
61
22
|
this.vibeEventEndpoint = (endPoint)
|
|
62
23
|
? endPoint
|
|
63
24
|
: this.config.vibeEventEndpoint;
|
|
@@ -68,70 +29,7 @@ class FlexPLMConnect {
|
|
|
68
29
|
this.staticHeaders = this.config?.flexplmConnect?.staticHeaders;
|
|
69
30
|
}
|
|
70
31
|
}
|
|
71
|
-
|
|
72
|
-
///////// ET-390 - Session reuse: start
|
|
73
|
-
/////////////////////////////////////////////////////////////////////////////
|
|
74
|
-
/*
|
|
75
|
-
private get sessionOptions() {
|
|
76
|
-
const sessReuseBool = this.config?.flexplmConnect?.sessionReuse;
|
|
77
|
-
console.info('sessReuseBool value is --> ' + sessReuseBool);
|
|
78
|
-
console.info('enabled value is --> ' + sessReuseBool?.enabled);
|
|
79
|
-
return {
|
|
80
|
-
enabled: sessReuseBool?.enabled === true || String(sessReuseBool?.enabled).toLowerCase() === 'true',
|
|
81
|
-
ttlMs: (sessReuseBool?.ttlSeconds ?? 900) * 1000,
|
|
82
|
-
cookieName: sessReuseBool?.cookieName ?? 'JSESSIONID',
|
|
83
|
-
retryStatuses: (sessReuseBool?.retryStatuses ?? [401, 403]) as number[],
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
*/
|
|
87
|
-
get sessionOptions() {
|
|
88
|
-
const cfg = this.config ?? {};
|
|
89
|
-
const sessReuseEntries = (name) => cfg[`flexplmConnect.sessionReuse.${name}`];
|
|
90
|
-
const enabled = sessReuseEntries('enabled');
|
|
91
|
-
return {
|
|
92
|
-
enabled: enabled === true || String(enabled).toLowerCase() === 'true',
|
|
93
|
-
ttlMs: parseTtlSeconds(sessReuseEntries('ttlSeconds')) * 1000,
|
|
94
|
-
cookieName: parseCookieName(sessReuseEntries('cookieName')),
|
|
95
|
-
retryStatuses: parseRetryStatuses(sessReuseEntries('retryStatuses')),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Waits until no cold session acquisition is running for this key.
|
|
100
|
-
* Callers must `await` this only when it returns a promise: awaiting even a resolved promise yields,
|
|
101
|
-
* and the check-then-register in runColdAcquisition has to stay synchronous to be race free.
|
|
102
|
-
*/
|
|
103
|
-
waitForInFlightAcquisition() {
|
|
104
|
-
const key = this.getSessionKey();
|
|
105
|
-
if (!inFlightAcquisitions.has(key))
|
|
106
|
-
return undefined;
|
|
107
|
-
return (async () => {
|
|
108
|
-
let pending = inFlightAcquisitions.get(key);
|
|
109
|
-
while (pending) {
|
|
110
|
-
await pending.catch(() => undefined);
|
|
111
|
-
pending = inFlightAcquisitions.get(key);
|
|
112
|
-
}
|
|
113
|
-
})();
|
|
114
|
-
}
|
|
115
|
-
/** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
|
|
116
|
-
async runColdAcquisition(acquire) {
|
|
117
|
-
const key = this.getSessionKey();
|
|
118
|
-
const acquisition = acquire();
|
|
119
|
-
inFlightAcquisitions.set(key, acquisition);
|
|
120
|
-
try {
|
|
121
|
-
return await acquisition;
|
|
122
|
-
}
|
|
123
|
-
finally {
|
|
124
|
-
if (inFlightAcquisitions.get(key) === acquisition)
|
|
125
|
-
inFlightAcquisitions.delete(key);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
getSessionKey() {
|
|
129
|
-
// Options that change the cached value are part of the key; JSON avoids '|' collisions between fields.
|
|
130
|
-
const { cookieName, ttlMs } = this.sessionOptions;
|
|
131
|
-
return JSON.stringify([
|
|
132
|
-
this.config.apiHost, this.config.urlContext, this.config.userName(), this.config.plmEnviornment, cookieName, ttlMs,
|
|
133
|
-
]);
|
|
134
|
-
}
|
|
32
|
+
// ET-390 - Session reuse
|
|
135
33
|
/** Releases the connection of a rejected response before it is replaced by a retry. */
|
|
136
34
|
async discardBody(response) {
|
|
137
35
|
try {
|
|
@@ -142,60 +40,6 @@ class FlexPLMConnect {
|
|
|
142
40
|
// best effort: nothing to do if the body is already consumed or locked
|
|
143
41
|
}
|
|
144
42
|
}
|
|
145
|
-
getCachedSession() {
|
|
146
|
-
if (!this.sessionOptions.enabled)
|
|
147
|
-
return undefined;
|
|
148
|
-
const key = this.getSessionKey();
|
|
149
|
-
const cachedSessValue = sessionCache.get(key);
|
|
150
|
-
if (!cachedSessValue)
|
|
151
|
-
return undefined;
|
|
152
|
-
if (cachedSessValue.expiresAt <= Date.now()) {
|
|
153
|
-
sessionCache.delete(key);
|
|
154
|
-
return undefined;
|
|
155
|
-
}
|
|
156
|
-
return cachedSessValue.cookie;
|
|
157
|
-
}
|
|
158
|
-
storeSession(cookie) {
|
|
159
|
-
// sliding expiry: Windchill's session timeout is also sliding
|
|
160
|
-
const key = this.getSessionKey();
|
|
161
|
-
// delete first so a refreshed entry moves to the newest position (oldest-first eviction then acts as LRU)
|
|
162
|
-
sessionCache.delete(key);
|
|
163
|
-
pruneSessionCache();
|
|
164
|
-
sessionCache.set(key, { cookie, expiresAt: Date.now() + this.sessionOptions.ttlMs });
|
|
165
|
-
}
|
|
166
|
-
clearSession() {
|
|
167
|
-
sessionCache.delete(this.getSessionKey());
|
|
168
|
-
}
|
|
169
|
-
/** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
|
|
170
|
-
extractSessionCookie(response) {
|
|
171
|
-
const respHeader = response?.headers;
|
|
172
|
-
if (!respHeader)
|
|
173
|
-
return undefined;
|
|
174
|
-
let raw = [];
|
|
175
|
-
if (typeof respHeader.getSetCookie === 'function') {
|
|
176
|
-
raw = respHeader.getSetCookie();
|
|
177
|
-
}
|
|
178
|
-
else if (typeof respHeader.get === 'function' && respHeader.get('set-cookie')) {
|
|
179
|
-
raw = [respHeader.get('set-cookie')];
|
|
180
|
-
}
|
|
181
|
-
const name = this.sessionOptions.cookieName;
|
|
182
|
-
const regexFilter = new RegExp('(?:^|[,\\s])' + escapeRegExp(name) + '=([^;,\\s]+)', 'g');
|
|
183
|
-
let jSession;
|
|
184
|
-
for (const line of raw) {
|
|
185
|
-
let cookieNameMatcher = regexFilter.exec(line);
|
|
186
|
-
while (cookieNameMatcher !== null) {
|
|
187
|
-
jSession = cookieNameMatcher[1];
|
|
188
|
-
cookieNameMatcher = regexFilter.exec(line);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return jSession;
|
|
192
|
-
}
|
|
193
|
-
cookieHeader(sessionCookie) {
|
|
194
|
-
return sessionCookie ? `${this.sessionOptions.cookieName}=${sessionCookie}` : undefined;
|
|
195
|
-
}
|
|
196
|
-
/////////////////////////////////////////////////////////////////////////////
|
|
197
|
-
///////// ET-390 - Session reuse: end
|
|
198
|
-
/////////////////////////////////////////////////////////////////////////////
|
|
199
43
|
/////////////////////////////////////////////////////////////////////////////
|
|
200
44
|
///////// Custom getRequestOptions: start
|
|
201
45
|
/////////////////////////////////////////////////////////////////////////////
|
|
@@ -213,17 +57,21 @@ class FlexPLMConnect {
|
|
|
213
57
|
Object.assign(csrfOptions.headers, this.staticHeaders);
|
|
214
58
|
}
|
|
215
59
|
// Added for Session reuse - ET-390 - Starts
|
|
216
|
-
|
|
60
|
+
// Merge with a configured static Cookie header (any header-name casing) instead of replacing it.
|
|
61
|
+
const headers = csrfOptions.headers;
|
|
62
|
+
const staticCookieKeys = Object.keys(headers).filter(key => key.toLowerCase() === 'cookie');
|
|
63
|
+
const staticCookie = staticCookieKeys.map(key => headers[key]).join('; ');
|
|
64
|
+
const cookie = this.sessions.buildCookieHeader(staticCookie, sessionCookie);
|
|
65
|
+
staticCookieKeys.forEach(key => delete headers[key]);
|
|
217
66
|
if (cookie) {
|
|
218
|
-
|
|
67
|
+
headers['Cookie'] = cookie;
|
|
219
68
|
}
|
|
220
69
|
// Added for Session reuse - ET-390 - Ends
|
|
221
70
|
if (app_framework_1.Logger.isInfoOn()) {
|
|
222
71
|
const logOptions = JSON.parse(JSON.stringify(csrfOptions));
|
|
223
72
|
logOptions['headers']['Authorization'] = logOptions?.headers?.Authorization.substring(0, 9);
|
|
224
73
|
// Added for Session reuse - ET-390 - Starts
|
|
225
|
-
|
|
226
|
-
logOptions.headers['Cookie'] = '***masked***';
|
|
74
|
+
maskCookieHeaders(logOptions.headers);
|
|
227
75
|
// Added for Session reuse - ET-390 - Ends
|
|
228
76
|
console.info('csrfOptions: ' + JSON.stringify(logOptions));
|
|
229
77
|
}
|
|
@@ -237,30 +85,30 @@ class FlexPLMConnect {
|
|
|
237
85
|
// rejectedCookie: the cookie that was rejected by the caller, so a refresh already done by a concurrent event is not thrown away
|
|
238
86
|
async getCSRF(getNewSession = false, rejectedCookie) {
|
|
239
87
|
// Added for Session reuse - ET-390 - Starts
|
|
240
|
-
if (!this.
|
|
88
|
+
if (!this.sessions.options.enabled)
|
|
241
89
|
return this.fetchCSRF(undefined);
|
|
242
|
-
for (let wait = this.waitForInFlightAcquisition(); wait; wait = this.waitForInFlightAcquisition())
|
|
90
|
+
for (let wait = this.sessions.waitForInFlightAcquisition(); wait; wait = this.sessions.waitForInFlightAcquisition())
|
|
243
91
|
await wait;
|
|
244
92
|
if (getNewSession) {
|
|
245
|
-
const current = this.getCachedSession();
|
|
93
|
+
const current = this.sessions.getCachedSession();
|
|
246
94
|
if (rejectedCookie === undefined || !current || current === rejectedCookie)
|
|
247
|
-
this.clearSession();
|
|
95
|
+
this.sessions.clearSession();
|
|
248
96
|
}
|
|
249
|
-
const cached = this.getCachedSession();
|
|
97
|
+
const cached = this.sessions.getCachedSession();
|
|
250
98
|
if (cached) {
|
|
251
99
|
try {
|
|
252
100
|
return await this.fetchCSRF(cached);
|
|
253
101
|
}
|
|
254
102
|
catch (e) {
|
|
255
|
-
if (!(e instanceof SessionRejectedError))
|
|
103
|
+
if (!(e instanceof flexplm_session_manager_1.SessionRejectedError))
|
|
256
104
|
throw e;
|
|
257
105
|
// if the cached cookie is rejected (401/403), drop it and acquire a new session (serialized)
|
|
258
|
-
if (this.getCachedSession() === cached)
|
|
259
|
-
this.clearSession();
|
|
106
|
+
if (this.sessions.getCachedSession() === cached)
|
|
107
|
+
this.sessions.clearSession();
|
|
260
108
|
return this.getCSRF();
|
|
261
109
|
}
|
|
262
110
|
}
|
|
263
|
-
return this.runColdAcquisition(() => this.fetchCSRF(undefined));
|
|
111
|
+
return this.sessions.runColdAcquisition(() => this.fetchCSRF(undefined));
|
|
264
112
|
// Added for Session reuse - ET-390 - Ends
|
|
265
113
|
}
|
|
266
114
|
async fetchCSRF(sentCookie) {
|
|
@@ -272,10 +120,10 @@ class FlexPLMConnect {
|
|
|
272
120
|
const csrfOptions = this.getRequestOptions('GET', sentCookie);
|
|
273
121
|
const response = await fetch(csrfURL, csrfOptions);
|
|
274
122
|
// Added for Session reuse - ET-390 - Starts
|
|
275
|
-
if (sentCookie && this.
|
|
123
|
+
if (sentCookie && this.sessions.options.retryStatuses.includes(response.status)) {
|
|
276
124
|
console.warn('Cached FlexPLM session rejected on csrf call (status ' + response.status + '). Retrying with a new session.');
|
|
277
125
|
await this.discardBody(response);
|
|
278
|
-
throw new SessionRejectedError();
|
|
126
|
+
throw new flexplm_session_manager_1.SessionRejectedError();
|
|
279
127
|
}
|
|
280
128
|
// Added for Session reuse - ET-390 - Ends
|
|
281
129
|
if (response.status >= 300) {
|
|
@@ -289,10 +137,10 @@ class FlexPLMConnect {
|
|
|
289
137
|
// Added for Session reuse - ET-390 - Starts
|
|
290
138
|
// If the cookie we sent was invalid, Windchill answers with a fresh Set-Cookie --> replace the cache.
|
|
291
139
|
let sessionCookie = sentCookie;
|
|
292
|
-
if (this.
|
|
293
|
-
sessionCookie = this.extractSessionCookie(response) ?? sentCookie;
|
|
140
|
+
if (this.sessions.options.enabled) {
|
|
141
|
+
sessionCookie = this.sessions.extractSessionCookie(response) ?? sentCookie;
|
|
294
142
|
if (sessionCookie)
|
|
295
|
-
this.storeSession(sessionCookie);
|
|
143
|
+
this.sessions.storeSession(sessionCookie, sentCookie);
|
|
296
144
|
}
|
|
297
145
|
// Added for Session reuse - ET-390 - Ends
|
|
298
146
|
try {
|
|
@@ -328,8 +176,7 @@ class FlexPLMConnect {
|
|
|
328
176
|
const logOptions = JSON.parse(JSON.stringify(xferOptions));
|
|
329
177
|
if (logOptions?.headers?.Authorization)
|
|
330
178
|
logOptions.headers.Authorization = logOptions?.headers?.Authorization.substring(0, 9);
|
|
331
|
-
|
|
332
|
-
logOptions.headers.Cookie = '***masked***';
|
|
179
|
+
maskCookieHeaders(logOptions?.headers);
|
|
333
180
|
if (csrf?.nonce_key)
|
|
334
181
|
logOptions.headers[csrf.nonce_key] = csrf.nonce ? `***masked(length:${csrf.nonce.length})***` : '***masked(empty)***';
|
|
335
182
|
console.info('csrfOptions: ' + JSON.stringify(logOptions));
|
|
@@ -356,7 +203,7 @@ class FlexPLMConnect {
|
|
|
356
203
|
/// Added for Session reuse - ET-390 - Starts
|
|
357
204
|
// const response = await this.sendRequest(csrf, payload);
|
|
358
205
|
let response = await this.sendRequest(csrf, payload);
|
|
359
|
-
if (csrf.sessionCookie && this.
|
|
206
|
+
if (csrf.sessionCookie && this.sessions.options.retryStatuses.includes(response.status)) {
|
|
360
207
|
console.warn('FlexPLM rejected session/nonce (status ' + response.status + '). Getting a new session and retrying once.');
|
|
361
208
|
await this.discardBody(response);
|
|
362
209
|
csrf = await this.getCSRF(true, csrf.sessionCookie);
|
|
@@ -417,30 +264,30 @@ class FlexPLMConnect {
|
|
|
417
264
|
const vibeEventsURL = this.config.apiHost + urlContext + path;
|
|
418
265
|
// Added for Session reuse - ET-390 - Starts
|
|
419
266
|
const attempt = async () => {
|
|
420
|
-
if (!this.
|
|
267
|
+
if (!this.sessions.options.enabled) {
|
|
421
268
|
return { response: await fetch(vibeEventsURL, this.getRequestOptions('GET')), sent: undefined };
|
|
422
269
|
}
|
|
423
|
-
for (let wait = this.waitForInFlightAcquisition(); wait; wait = this.waitForInFlightAcquisition())
|
|
270
|
+
for (let wait = this.sessions.waitForInFlightAcquisition(); wait; wait = this.sessions.waitForInFlightAcquisition())
|
|
424
271
|
await wait;
|
|
425
|
-
const sent = this.getCachedSession();
|
|
272
|
+
const sent = this.sessions.getCachedSession();
|
|
426
273
|
const call = async () => {
|
|
427
274
|
const res = await fetch(vibeEventsURL, this.getRequestOptions('GET', sent));
|
|
428
275
|
// A rejected cookie must not be stored again; only keep a cookie that was accepted or newly issued.
|
|
429
|
-
if (!(sent && this.
|
|
430
|
-
const cookie = this.extractSessionCookie(res) ?? sent;
|
|
276
|
+
if (!(sent && this.sessions.options.retryStatuses.includes(res.status))) {
|
|
277
|
+
const cookie = this.sessions.extractSessionCookie(res) ?? sent;
|
|
431
278
|
if (cookie)
|
|
432
|
-
this.storeSession(cookie);
|
|
279
|
+
this.sessions.storeSession(cookie, sent);
|
|
433
280
|
}
|
|
434
281
|
return { response: res, sent };
|
|
435
282
|
};
|
|
436
|
-
return sent ? call() : this.runColdAcquisition(call);
|
|
283
|
+
return sent ? call() : this.sessions.runColdAcquisition(call);
|
|
437
284
|
};
|
|
438
285
|
const first = await attempt();
|
|
439
286
|
let response = first.response;
|
|
440
|
-
if (first.sent && this.
|
|
287
|
+
if (first.sent && this.sessions.options.retryStatuses.includes(response.status)) {
|
|
441
288
|
await this.discardBody(response);
|
|
442
|
-
if (this.getCachedSession() === first.sent)
|
|
443
|
-
this.clearSession();
|
|
289
|
+
if (this.sessions.getCachedSession() === first.sent)
|
|
290
|
+
this.sessions.clearSession();
|
|
444
291
|
response = (await attempt()).response;
|
|
445
292
|
}
|
|
446
293
|
// Added for Session reuse - ET-390 - Ends
|
|
@@ -39,12 +39,8 @@ const resp = (status, setCookie = [], json = csrfBody) => ({
|
|
|
39
39
|
});
|
|
40
40
|
let userCounter = 0;
|
|
41
41
|
const uniqueUser = () => 'sess-user-' + (++userCounter);
|
|
42
|
-
//
|
|
43
|
-
const sessionConfig = (user, sessionReuse = { enabled: true }) => {
|
|
44
|
-
const flat = {};
|
|
45
|
-
Object.entries(sessionReuse).forEach(([k, v]) => { flat[`flexplmConnect.sessionReuse.${k}`] = v; });
|
|
46
|
-
return createConfig({ userName: () => user, ...flat });
|
|
47
|
-
};
|
|
42
|
+
// Session options are nested: { flexplmConnect: { sessionReuse: { enabled, ... } } }
|
|
43
|
+
const sessionConfig = (user, sessionReuse = { enabled: true }) => createConfig({ userName: () => user, flexplmConnect: { sessionReuse } });
|
|
48
44
|
const fetchMock = () => global.fetch;
|
|
49
45
|
const callHeaders = (i) => fetchMock().mock.calls[i][1].headers;
|
|
50
46
|
const callMethod = (i) => fetchMock().mock.calls[i][1].method;
|
|
@@ -358,6 +354,60 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
358
354
|
isInfoOn.mockRestore();
|
|
359
355
|
info.mockRestore();
|
|
360
356
|
});
|
|
357
|
+
describe('static Cookie header', () => {
|
|
358
|
+
const withStatic = (user, staticHeaders, sessionReuse = { enabled: true }) => createConfig({ userName: () => user, flexplmConnect: { sessionReuse, staticHeaders } });
|
|
359
|
+
it('should keep other static cookies and replace only the managed one once a session is cached', async () => {
|
|
360
|
+
const u = uniqueUser();
|
|
361
|
+
fetchMock()
|
|
362
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=new.tomcat1; Path=/']))
|
|
363
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
364
|
+
await new flexplm_connect_1.FlexPLMConnect(withStatic(u, { Cookie: 'lb=abc; JSESSIONID=stale; theme=dark' })).sendToFlexPLM(payload);
|
|
365
|
+
expect(callHeaders(0).Cookie).toBe('lb=abc; theme=dark'); // cold: stale managed cookie dropped
|
|
366
|
+
expect(callHeaders(1).Cookie).toBe('lb=abc; theme=dark; JSESSIONID=new.tomcat1');
|
|
367
|
+
});
|
|
368
|
+
it('should send a single Cookie header when the static header name is lowercase', async () => {
|
|
369
|
+
const u = uniqueUser();
|
|
370
|
+
fetchMock()
|
|
371
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1; Path=/']))
|
|
372
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
373
|
+
await new flexplm_connect_1.FlexPLMConnect(withStatic(u, { cookie: 'lb=abc' })).sendToFlexPLM(payload);
|
|
374
|
+
const postHeaders = callHeaders(1);
|
|
375
|
+
expect(Object.keys(postHeaders).filter(k => k.toLowerCase() === 'cookie')).toEqual(['Cookie']);
|
|
376
|
+
expect(postHeaders.Cookie).toBe('lb=abc; JSESSIONID=abc.tomcat1');
|
|
377
|
+
});
|
|
378
|
+
it('should send the static Cookie unchanged when session reuse is disabled', async () => {
|
|
379
|
+
const u = uniqueUser();
|
|
380
|
+
fetchMock()
|
|
381
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1; Path=/']))
|
|
382
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
383
|
+
await new flexplm_connect_1.FlexPLMConnect(withStatic(u, { Cookie: 'lb=abc; JSESSIONID=static' }, { enabled: false })).sendToFlexPLM(payload);
|
|
384
|
+
expect(callHeaders(0).Cookie).toBe('lb=abc; JSESSIONID=static');
|
|
385
|
+
expect(callHeaders(1).Cookie).toBe('lb=abc; JSESSIONID=static');
|
|
386
|
+
});
|
|
387
|
+
it('should not resend a rejected static JSESSIONID on the fresh retry', async () => {
|
|
388
|
+
const u = uniqueUser();
|
|
389
|
+
fetchMock()
|
|
390
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old.tomcat1; Path=/']))
|
|
391
|
+
.mockResolvedValueOnce(resp(403, [], {})) // POST rejected
|
|
392
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=fresh.tomcat1; Path=/'])) // fresh GET
|
|
393
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
394
|
+
await new flexplm_connect_1.FlexPLMConnect(withStatic(u, { Cookie: 'lb=abc; JSESSIONID=static' })).sendToFlexPLM(payload);
|
|
395
|
+
expect(callHeaders(2).Cookie).toBe('lb=abc'); // fresh GET carries no JSESSIONID
|
|
396
|
+
expect(callHeaders(3).Cookie).toBe('lb=abc; JSESSIONID=fresh.tomcat1');
|
|
397
|
+
});
|
|
398
|
+
it('should mask a lowercase static cookie header in logs', async () => {
|
|
399
|
+
const isInfoOn = jest.spyOn(app_framework_1.Logger, 'isInfoOn').mockReturnValue(true);
|
|
400
|
+
const info = jest.spyOn(console, 'info').mockImplementation();
|
|
401
|
+
fetchMock()
|
|
402
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=abc.tomcat1']))
|
|
403
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
404
|
+
await new flexplm_connect_1.FlexPLMConnect(withStatic(uniqueUser(), { cookie: 'lb=topsecret' }, { enabled: false })).sendToFlexPLM(payload);
|
|
405
|
+
const logged = info.mock.calls.map(c => String(c[0] ?? '')).join('\n');
|
|
406
|
+
expect(logged).not.toContain('topsecret');
|
|
407
|
+
isInfoOn.mockRestore();
|
|
408
|
+
info.mockRestore();
|
|
409
|
+
});
|
|
410
|
+
});
|
|
361
411
|
describe('option normalisation', () => {
|
|
362
412
|
const rejectedOnceThenOk = (u, opts) => {
|
|
363
413
|
fetchMock()
|
|
@@ -525,6 +575,41 @@ describe('FlexPLMConnect session reuse', () => {
|
|
|
525
575
|
describe('concurrent session acquisition', () => {
|
|
526
576
|
const flush = () => new Promise(r => setImmediate(r));
|
|
527
577
|
const isColdGet = (call) => call[1].method === 'GET' && !call[1].headers.Cookie;
|
|
578
|
+
it('should not let a late response using an older cookie roll back a refreshed session', async () => {
|
|
579
|
+
const u = uniqueUser();
|
|
580
|
+
const cfg = () => sessionConfig(u);
|
|
581
|
+
// seed the cache with 'old'
|
|
582
|
+
fetchMock()
|
|
583
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=old']))
|
|
584
|
+
.mockResolvedValueOnce(resp(200, [], {}));
|
|
585
|
+
await new flexplm_connect_1.FlexPLMConnect(cfg()).sendToFlexPLM(payload);
|
|
586
|
+
let lateGetsSeen = 0;
|
|
587
|
+
let releaseLate;
|
|
588
|
+
const lateGate = new Promise(r => { releaseLate = r; });
|
|
589
|
+
fetchMock().mockReset();
|
|
590
|
+
fetchMock().mockImplementation(async (_url, opts) => {
|
|
591
|
+
const cookie = opts.headers.Cookie;
|
|
592
|
+
if (opts.method === 'GET' && cookie === 'JSESSIONID=old') {
|
|
593
|
+
if (++lateGetsSeen === 1)
|
|
594
|
+
return resp(403, []); // event A: old rejected
|
|
595
|
+
await lateGate; // event B: slow 200, no Set-Cookie
|
|
596
|
+
return resp(200, []);
|
|
597
|
+
}
|
|
598
|
+
if (opts.method === 'GET' && !cookie)
|
|
599
|
+
return resp(200, ['JSESSIONID=new']); // A's fresh acquisition
|
|
600
|
+
if (opts.method === 'GET')
|
|
601
|
+
return resp(200, []);
|
|
602
|
+
return resp(200, [], {});
|
|
603
|
+
});
|
|
604
|
+
const a = new flexplm_connect_1.FlexPLMConnect(cfg()).sendToFlexPLM(payload);
|
|
605
|
+
const b = new flexplm_connect_1.FlexPLMConnect(cfg()).sendToFlexPLM(payload);
|
|
606
|
+
await a;
|
|
607
|
+
releaseLate();
|
|
608
|
+
await b;
|
|
609
|
+
fetchMock().mockClear();
|
|
610
|
+
await new flexplm_connect_1.FlexPLMConnect(cfg()).getCSRF();
|
|
611
|
+
expect(callHeaders(0).Cookie).toBe('JSESSIONID=new'); // B must not have stored 'old' again
|
|
612
|
+
});
|
|
528
613
|
it('should share one session across concurrent events on a cold cache', async () => {
|
|
529
614
|
const u = uniqueUser();
|
|
530
615
|
let release;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { FCConfig } from '../interfaces/interfaces';
|
|
2
|
+
export declare class SessionRejectedError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/** Owns the FlexPLM session cookie cache: options, keying, expiry, in-flight acquisition sharing and Set-Cookie parsing. */
|
|
5
|
+
export declare class FlexPLMSessionManager {
|
|
6
|
+
private config;
|
|
7
|
+
constructor(config: FCConfig);
|
|
8
|
+
get options(): {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
ttlMs: number;
|
|
11
|
+
cookieName: string;
|
|
12
|
+
retryStatuses: number[];
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Waits until no cold session acquisition is running for this key.
|
|
16
|
+
* Callers must `await` this only when it returns a promise: awaiting even a resolved promise yields,
|
|
17
|
+
* and the check-then-register in runColdAcquisition has to stay synchronous to be race free.
|
|
18
|
+
*/
|
|
19
|
+
waitForInFlightAcquisition(): Promise<void> | undefined;
|
|
20
|
+
/** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
|
|
21
|
+
runColdAcquisition<T>(acquire: () => Promise<T>): Promise<T>;
|
|
22
|
+
private getSessionKey;
|
|
23
|
+
getCachedSession(): string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Compare-and-set: stores `cookie` only if the cache still holds the cookie this request was sent with
|
|
26
|
+
* (`sentCookie`, undefined for a cold acquisition = nothing cached). A late response must not roll back or
|
|
27
|
+
* resurrect a session that a concurrent event already replaced or cleared.
|
|
28
|
+
* @returns true if the cache was updated
|
|
29
|
+
*/
|
|
30
|
+
storeSession(cookie: string, sentCookie: string | undefined): boolean;
|
|
31
|
+
clearSession(): void;
|
|
32
|
+
/** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
|
|
33
|
+
extractSessionCookie(response: Response): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Builds the Cookie header value from the configured static Cookie header and the managed session cookie.
|
|
36
|
+
* With session reuse enabled only the managed cookie is replaced/removed; every other static cookie pair is kept.
|
|
37
|
+
* With session reuse disabled the static value is returned unchanged.
|
|
38
|
+
*/
|
|
39
|
+
buildCookieHeader(staticCookie?: string, sessionCookie?: string): string | undefined;
|
|
40
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlexPLMSessionManager = exports.SessionRejectedError = void 0;
|
|
4
|
+
// Module-level on purpose: a new FlexPLMConnect (and session manager) is constructed for every event.
|
|
5
|
+
const sessionCache = new Map();
|
|
6
|
+
// Upper bound so users/option combinations that are never requested again cannot accumulate cookies forever.
|
|
7
|
+
const MAX_CACHED_SESSIONS = 100;
|
|
8
|
+
const pruneSessionCache = () => {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
for (const [key, session] of sessionCache) {
|
|
11
|
+
if (session.expiresAt <= now)
|
|
12
|
+
sessionCache.delete(key);
|
|
13
|
+
}
|
|
14
|
+
// Still full: drop the oldest entries (Map keeps insertion order).
|
|
15
|
+
while (sessionCache.size >= MAX_CACHED_SESSIONS) {
|
|
16
|
+
sessionCache.delete(sessionCache.keys().next().value);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
// In-flight cold session acquisitions per key, so concurrent events share one FlexPLM session instead of each creating their own.
|
|
20
|
+
const inFlightAcquisitions = new Map();
|
|
21
|
+
const DEFAULT_SESSION_COOKIE_NAME = 'JSESSIONID';
|
|
22
|
+
const DEFAULT_SESSION_TTL_SECONDS = 900;
|
|
23
|
+
const DEFAULT_RETRY_STATUSES = [401, 403];
|
|
24
|
+
const COOKIE_NAME_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
25
|
+
// Thrown internally when a cached cookie was rejected, so the caller can re-acquire under the lock.
|
|
26
|
+
class SessionRejectedError extends Error {
|
|
27
|
+
}
|
|
28
|
+
exports.SessionRejectedError = SessionRejectedError;
|
|
29
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
|
+
const parseRetryStatuses = (value) => {
|
|
31
|
+
let list = [];
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
list = value;
|
|
34
|
+
else if (typeof value === 'string')
|
|
35
|
+
list = value.split(',');
|
|
36
|
+
else if (value !== undefined && value !== null)
|
|
37
|
+
list = [value];
|
|
38
|
+
const statuses = list
|
|
39
|
+
.map(status => (typeof status === 'string' ? status.trim() : status))
|
|
40
|
+
.filter(status => status !== '' && status !== null && status !== undefined)
|
|
41
|
+
.map(Number)
|
|
42
|
+
// Only client-error statuses can mean "session rejected". 2xx/5xx would resend events the server already processed.
|
|
43
|
+
.filter(status => Number.isInteger(status) && status >= 400 && status <= 499);
|
|
44
|
+
return statuses.length ? statuses : DEFAULT_RETRY_STATUSES;
|
|
45
|
+
};
|
|
46
|
+
const parseTtlSeconds = (value) => {
|
|
47
|
+
const ttl = Number(value);
|
|
48
|
+
return Number.isFinite(ttl) && ttl > 0 ? ttl : DEFAULT_SESSION_TTL_SECONDS;
|
|
49
|
+
};
|
|
50
|
+
const parseCookieName = (value) => {
|
|
51
|
+
const name = typeof value === 'string' ? value.trim() : '';
|
|
52
|
+
return COOKIE_NAME_TOKEN.test(name) ? name : DEFAULT_SESSION_COOKIE_NAME;
|
|
53
|
+
};
|
|
54
|
+
/** Owns the FlexPLM session cookie cache: options, keying, expiry, in-flight acquisition sharing and Set-Cookie parsing. */
|
|
55
|
+
class FlexPLMSessionManager {
|
|
56
|
+
constructor(config) {
|
|
57
|
+
this.config = config;
|
|
58
|
+
}
|
|
59
|
+
get options() {
|
|
60
|
+
const sessionReuseConfig = this.config?.flexplmConnect?.sessionReuse ?? {};
|
|
61
|
+
return {
|
|
62
|
+
enabled: sessionReuseConfig.enabled === true || String(sessionReuseConfig.enabled).toLowerCase() === 'true',
|
|
63
|
+
ttlMs: parseTtlSeconds(sessionReuseConfig.ttlSeconds) * 1000,
|
|
64
|
+
cookieName: parseCookieName(sessionReuseConfig.cookieName),
|
|
65
|
+
retryStatuses: parseRetryStatuses(sessionReuseConfig.retryStatuses),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Waits until no cold session acquisition is running for this key.
|
|
70
|
+
* Callers must `await` this only when it returns a promise: awaiting even a resolved promise yields,
|
|
71
|
+
* and the check-then-register in runColdAcquisition has to stay synchronous to be race free.
|
|
72
|
+
*/
|
|
73
|
+
waitForInFlightAcquisition() {
|
|
74
|
+
const key = this.getSessionKey();
|
|
75
|
+
if (!inFlightAcquisitions.has(key))
|
|
76
|
+
return undefined;
|
|
77
|
+
return (async () => {
|
|
78
|
+
let pending = inFlightAcquisitions.get(key);
|
|
79
|
+
while (pending) {
|
|
80
|
+
await pending.catch(() => undefined);
|
|
81
|
+
pending = inFlightAcquisitions.get(key);
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
}
|
|
85
|
+
/** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
|
|
86
|
+
async runColdAcquisition(acquire) {
|
|
87
|
+
const key = this.getSessionKey();
|
|
88
|
+
const acquisition = acquire();
|
|
89
|
+
inFlightAcquisitions.set(key, acquisition);
|
|
90
|
+
try {
|
|
91
|
+
return await acquisition;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (inFlightAcquisitions.get(key) === acquisition)
|
|
95
|
+
inFlightAcquisitions.delete(key);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
getSessionKey() {
|
|
99
|
+
// Options that change the cached value are part of the key; JSON avoids '|' collisions between fields.
|
|
100
|
+
const { cookieName, ttlMs } = this.options;
|
|
101
|
+
return JSON.stringify([
|
|
102
|
+
this.config.apiHost, this.config.urlContext, this.config.userName(), this.config.plmEnviornment, cookieName, ttlMs,
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
getCachedSession() {
|
|
106
|
+
if (!this.options.enabled)
|
|
107
|
+
return undefined;
|
|
108
|
+
const key = this.getSessionKey();
|
|
109
|
+
const cachedSessValue = sessionCache.get(key);
|
|
110
|
+
if (!cachedSessValue)
|
|
111
|
+
return undefined;
|
|
112
|
+
if (cachedSessValue.expiresAt <= Date.now()) {
|
|
113
|
+
sessionCache.delete(key);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return cachedSessValue.cookie;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Compare-and-set: stores `cookie` only if the cache still holds the cookie this request was sent with
|
|
120
|
+
* (`sentCookie`, undefined for a cold acquisition = nothing cached). A late response must not roll back or
|
|
121
|
+
* resurrect a session that a concurrent event already replaced or cleared.
|
|
122
|
+
* @returns true if the cache was updated
|
|
123
|
+
*/
|
|
124
|
+
storeSession(cookie, sentCookie) {
|
|
125
|
+
const key = this.getSessionKey();
|
|
126
|
+
if (sessionCache.get(key)?.cookie !== sentCookie)
|
|
127
|
+
return false;
|
|
128
|
+
// sliding expiry: Windchill's session timeout is also sliding
|
|
129
|
+
// delete first so a refreshed entry moves to the newest position (oldest-first eviction then acts as LRU)
|
|
130
|
+
sessionCache.delete(key);
|
|
131
|
+
pruneSessionCache();
|
|
132
|
+
sessionCache.set(key, { cookie, expiresAt: Date.now() + this.options.ttlMs });
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
clearSession() {
|
|
136
|
+
sessionCache.delete(this.getSessionKey());
|
|
137
|
+
}
|
|
138
|
+
/** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
|
|
139
|
+
extractSessionCookie(response) {
|
|
140
|
+
const respHeader = response?.headers;
|
|
141
|
+
if (!respHeader)
|
|
142
|
+
return undefined;
|
|
143
|
+
let raw = [];
|
|
144
|
+
if (typeof respHeader.getSetCookie === 'function') {
|
|
145
|
+
raw = respHeader.getSetCookie();
|
|
146
|
+
}
|
|
147
|
+
else if (typeof respHeader.get === 'function' && respHeader.get('set-cookie')) {
|
|
148
|
+
raw = [respHeader.get('set-cookie')];
|
|
149
|
+
}
|
|
150
|
+
const name = this.options.cookieName;
|
|
151
|
+
const regexFilter = new RegExp('(?:^|[,\\s])' + escapeRegExp(name) + '=([^;,\\s]+)', 'g');
|
|
152
|
+
let jSession;
|
|
153
|
+
for (const line of raw) {
|
|
154
|
+
let cookieNameMatcher = regexFilter.exec(line);
|
|
155
|
+
while (cookieNameMatcher !== null) {
|
|
156
|
+
jSession = cookieNameMatcher[1];
|
|
157
|
+
cookieNameMatcher = regexFilter.exec(line);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return jSession;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Builds the Cookie header value from the configured static Cookie header and the managed session cookie.
|
|
164
|
+
* With session reuse enabled only the managed cookie is replaced/removed; every other static cookie pair is kept.
|
|
165
|
+
* With session reuse disabled the static value is returned unchanged.
|
|
166
|
+
*/
|
|
167
|
+
buildCookieHeader(staticCookie, sessionCookie) {
|
|
168
|
+
if (!this.options.enabled)
|
|
169
|
+
return staticCookie || undefined;
|
|
170
|
+
const name = this.options.cookieName;
|
|
171
|
+
const pairs = (staticCookie ?? '')
|
|
172
|
+
.split(';')
|
|
173
|
+
.map(pair => pair.trim())
|
|
174
|
+
.filter(pair => pair && pair.split('=')[0].trim() !== name);
|
|
175
|
+
if (sessionCookie)
|
|
176
|
+
pairs.push(`${name}=${sessionCookie}`);
|
|
177
|
+
return pairs.length ? pairs.join('; ') : undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
exports.FlexPLMSessionManager = FlexPLMSessionManager;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const flexplm_session_manager_1 = require("./flexplm-session-manager");
|
|
4
|
+
let userCounter = 0;
|
|
5
|
+
const newManager = () => {
|
|
6
|
+
const user = 'mgr-user-' + (++userCounter);
|
|
7
|
+
const config = {
|
|
8
|
+
apiHost: 'https://flexplm.example.com',
|
|
9
|
+
urlContext: '/FlexPLM',
|
|
10
|
+
vibeEventEndpoint: '/api/events',
|
|
11
|
+
csrfEndpoint: '/csrf',
|
|
12
|
+
plmEnviornment: 'test',
|
|
13
|
+
userName: () => user,
|
|
14
|
+
password: () => 'pass',
|
|
15
|
+
itemPreDevelopmentLifecycleStages: [],
|
|
16
|
+
flexplmConnect: { sessionReuse: { enabled: true } },
|
|
17
|
+
};
|
|
18
|
+
return new flexplm_session_manager_1.FlexPLMSessionManager(config);
|
|
19
|
+
};
|
|
20
|
+
describe('FlexPLMSessionManager.storeSession (compare-and-set)', () => {
|
|
21
|
+
it('should store a cold acquisition when nothing is cached', () => {
|
|
22
|
+
const m = newManager();
|
|
23
|
+
expect(m.storeSession('a', undefined)).toBe(true);
|
|
24
|
+
expect(m.getCachedSession()).toBe('a');
|
|
25
|
+
});
|
|
26
|
+
it('should not let a cold acquisition overwrite an entry that appeared meanwhile', () => {
|
|
27
|
+
const m = newManager();
|
|
28
|
+
m.storeSession('a', undefined);
|
|
29
|
+
expect(m.storeSession('b', undefined)).toBe(false);
|
|
30
|
+
expect(m.getCachedSession()).toBe('a');
|
|
31
|
+
});
|
|
32
|
+
it('should replace or refresh the entry while it still holds the cookie that was sent', () => {
|
|
33
|
+
const m = newManager();
|
|
34
|
+
m.storeSession('old', undefined);
|
|
35
|
+
expect(m.storeSession('new', 'old')).toBe(true);
|
|
36
|
+
expect(m.getCachedSession()).toBe('new');
|
|
37
|
+
expect(m.storeSession('new', 'new')).toBe(true); // sliding refresh with the same cookie
|
|
38
|
+
});
|
|
39
|
+
it('should ignore a late response that was sent with an older cookie', () => {
|
|
40
|
+
const m = newManager();
|
|
41
|
+
m.storeSession('old', undefined);
|
|
42
|
+
m.storeSession('new', 'old');
|
|
43
|
+
expect(m.storeSession('old', 'old')).toBe(false);
|
|
44
|
+
expect(m.getCachedSession()).toBe('new');
|
|
45
|
+
});
|
|
46
|
+
it('should not resurrect a session that was cleared meanwhile', () => {
|
|
47
|
+
const m = newManager();
|
|
48
|
+
m.storeSession('old', undefined);
|
|
49
|
+
m.clearSession();
|
|
50
|
+
expect(m.storeSession('old', 'old')).toBe(false);
|
|
51
|
+
expect(m.getCachedSession()).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
});
|