@queue-it/fastly 2.0.3-beta.4 → 4.4.3-beta.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 +22 -0
- package/package.json +4 -2
- package/src/contextProvider.ts +116 -71
- package/src/helper.ts +1 -1
- package/src/index.ts +1 -1
- package/src/integrationConfigProvider.ts +57 -2
- package/src/requestResponseHandler.ts +113 -58
package/README.md
CHANGED
|
@@ -109,6 +109,28 @@ if (res != null) {
|
|
|
109
109
|
- Create desired waiting room(s), triggers, and actions in the Go Queue-It self-service platform.
|
|
110
110
|
Then, save/publish the configuration.
|
|
111
111
|
|
|
112
|
+
## Enqueue Token Settings
|
|
113
|
+
|
|
114
|
+
The connector supports enqueue tokens, which are used to secure the queue redirect flow. These settings are read at runtime from the Fastly `IntegrationConfiguration` Config Store. They are optional — if not set, the defaults below apply.
|
|
115
|
+
|
|
116
|
+
| Key | Default | Description |
|
|
117
|
+
|-----|---------|-------------|
|
|
118
|
+
| `enqueueTokenEnabled` | `"true"` | Enables enqueue token generation. Set to `"false"` to disable. |
|
|
119
|
+
| `enqueueTokenValidityTime` | `"240000"` | Token validity time in milliseconds. |
|
|
120
|
+
| `enqueueTokenKeyEnabled` | `"false"` | Enables key-based enqueue tokens. Set to `"true"` to enable. |
|
|
121
|
+
|
|
122
|
+
To configure these, add them as items in the `IntegrationConfiguration` Config Store alongside the other settings (`customerId`, `apiKey`, `secret`, etc.) — either via the Fastly UI or the Fastly API.
|
|
123
|
+
|
|
124
|
+
## Request Body Trigger Matching
|
|
125
|
+
|
|
126
|
+
The connector supports matching triggers based on the request body content. This is disabled by default. When enabled, up to 2048 characters of the request body are read and passed to the SDK for trigger evaluation.
|
|
127
|
+
|
|
128
|
+
To enable it, add the following key to the `IntegrationConfiguration` Config Store:
|
|
129
|
+
|
|
130
|
+
| Key | Default | Description |
|
|
131
|
+
|-----|---------|-------------|
|
|
132
|
+
| `requestBodyEnabled` | `"false"` | Set to `"true"` to enable request body trigger matching. |
|
|
133
|
+
|
|
112
134
|
## Providing the queue configuration
|
|
113
135
|
|
|
114
136
|
The recommended way is to use the Go Queue-it self-service portal to setup the configuration. The configuration
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@queue-it/fastly",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.4.3-beta.0",
|
|
4
4
|
"description": "Queue-it connector for Fastly",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"author": "devs@queue-it.com",
|
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
"src/index.ts"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
21
22
|
"bundle": "esbuild src/index.ts --bundle --outfile=bin/index.js --platform=node --target=es2022 && js-compute-runtime bin/index.js bin/main.wasm",
|
|
22
23
|
"build": "fastly compute build",
|
|
23
24
|
"deploy": "fastly compute deploy"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
27
|
"@fastly/js-compute": "^3.0.0",
|
|
27
|
-
"@queue-it/connector-javascript": "^
|
|
28
|
+
"@queue-it/connector-javascript": "^4.4.4",
|
|
29
|
+
"@queue-it/queue-token": "~1.0.4"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
32
|
"esbuild": "^0.27.3",
|
package/src/contextProvider.ts
CHANGED
|
@@ -1,127 +1,172 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DefaultEnqueueTokenProvider,
|
|
3
|
+
IConnectorContextProvider,
|
|
4
|
+
ICryptoProvider,
|
|
5
|
+
IEnqueueTokenProvider,
|
|
6
|
+
INormalizationProvider,
|
|
7
|
+
DefaultNormalizationProvider,
|
|
8
|
+
IHttpRequest,
|
|
9
|
+
IHttpResponse,
|
|
10
|
+
} from '@queue-it/connector-javascript';
|
|
11
|
+
import { Token, Payload } from '@queue-it/queue-token';
|
|
2
12
|
import { FastlyCryptoProvider } from './fastlyCryptoProvider';
|
|
3
13
|
|
|
4
|
-
export function getHttpHandler(req: Request): FastlyHttpContextProvider {
|
|
5
|
-
return new FastlyHttpContextProvider(req);
|
|
14
|
+
export function getHttpHandler(req: Request, clientIp: string = '', bodyString: string = ''): FastlyHttpContextProvider {
|
|
15
|
+
return new FastlyHttpContextProvider(req, clientIp, bodyString);
|
|
6
16
|
}
|
|
7
17
|
|
|
8
18
|
export class FastlyHttpContextProvider implements IConnectorContextProvider {
|
|
9
19
|
isError: boolean = false;
|
|
10
|
-
private readonly
|
|
11
|
-
private readonly
|
|
20
|
+
private readonly _httpRequest: FastlyHttpRequest;
|
|
21
|
+
private readonly _httpResponse: FastlyHttpResponse;
|
|
22
|
+
private _enqueueTokenProvider: IEnqueueTokenProvider | null = null;
|
|
23
|
+
private readonly _normalizationProvider: INormalizationProvider;
|
|
24
|
+
private readonly _cryptoProvider: ICryptoProvider;
|
|
12
25
|
|
|
13
|
-
constructor(fReq: Request) {
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
26
|
+
constructor(fReq: Request, clientIp: string = '', bodyString: string = '') {
|
|
27
|
+
this._httpRequest = new FastlyHttpRequest(fReq, clientIp, bodyString);
|
|
28
|
+
this._httpResponse = new FastlyHttpResponse();
|
|
29
|
+
this._normalizationProvider = new DefaultNormalizationProvider();
|
|
30
|
+
this._cryptoProvider = new FastlyCryptoProvider();
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
getHttpRequest(): IHttpRequest {
|
|
19
|
-
return this.
|
|
34
|
+
return this._httpRequest;
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
getHttpResponse(): IHttpResponse {
|
|
23
|
-
return this.
|
|
38
|
+
return this._httpResponse;
|
|
24
39
|
}
|
|
25
40
|
|
|
26
|
-
getCryptoProvider():
|
|
27
|
-
return
|
|
41
|
+
getCryptoProvider(): ICryptoProvider {
|
|
42
|
+
return this._cryptoProvider;
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
setEnqueueTokenProvider(
|
|
46
|
+
customerId: string,
|
|
47
|
+
secretKey: string,
|
|
48
|
+
validityTime: number,
|
|
49
|
+
clientIp: string,
|
|
50
|
+
withKey: boolean
|
|
51
|
+
): void {
|
|
52
|
+
this._enqueueTokenProvider = new DefaultEnqueueTokenProvider(
|
|
53
|
+
customerId,
|
|
54
|
+
secretKey,
|
|
55
|
+
validityTime,
|
|
56
|
+
clientIp,
|
|
57
|
+
withKey,
|
|
58
|
+
Token,
|
|
59
|
+
Payload
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setCustomEnqueueTokenProvider(provider: IEnqueueTokenProvider): void {
|
|
64
|
+
this._enqueueTokenProvider = provider;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getEnqueueTokenProvider(): IEnqueueTokenProvider | null {
|
|
68
|
+
return this._enqueueTokenProvider;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getNormalizationProvider(): INormalizationProvider {
|
|
72
|
+
return this._normalizationProvider;
|
|
32
73
|
}
|
|
33
74
|
|
|
34
75
|
getResponseHeaders(): Headers {
|
|
35
|
-
return
|
|
76
|
+
return this._httpResponse.getHeaders();
|
|
36
77
|
}
|
|
37
78
|
}
|
|
38
79
|
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
private bodyFetched: boolean = false;
|
|
42
|
-
private body: string = '';
|
|
80
|
+
class FastlyHttpRequest implements IHttpRequest {
|
|
81
|
+
private _parsedCookieDic: Record<string, string> | null = null;
|
|
43
82
|
|
|
44
|
-
constructor(private baseReq: Request) {
|
|
45
|
-
this.parsedCookieDic = new Map();
|
|
46
|
-
this.bodyFetched = false;
|
|
47
|
-
}
|
|
83
|
+
constructor(private baseReq: Request, private _clientIp: string = '', private _bodyString: string = '') {}
|
|
48
84
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
for (let i = 0; i < cookies.length; i++) {
|
|
52
|
-
let cookieKV = cookies[i].split('=', 2);
|
|
53
|
-
if (cookieKV.length >= 2) {
|
|
54
|
-
this.parsedCookieDic.set(cookieKV[0].trim(), cookieKV[1].trim())
|
|
55
|
-
}
|
|
56
|
-
}
|
|
85
|
+
getUserAgent(): string {
|
|
86
|
+
return this.getHeader('user-agent');
|
|
57
87
|
}
|
|
58
88
|
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
return;
|
|
62
|
-
|
|
63
|
-
this.
|
|
64
|
-
this.body = ''; // this.context.req.text()
|
|
89
|
+
getHeader(name: string): string {
|
|
90
|
+
if (name.toLowerCase() === 'x-queueit-clientip')
|
|
91
|
+
return this.getUserHostAddress();
|
|
92
|
+
|
|
93
|
+
return this.baseReq.headers.get(name) || '';
|
|
65
94
|
}
|
|
66
95
|
|
|
67
96
|
getAbsoluteUri(): string {
|
|
68
97
|
return this.baseReq.url;
|
|
69
98
|
}
|
|
70
99
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.parseCookies(this.getHeader('cookie'))
|
|
74
|
-
}
|
|
75
|
-
if (!this.parsedCookieDic.has(cookieKey)) return undefined;
|
|
76
|
-
const cookieVal = this.parsedCookieDic.get(cookieKey)!;
|
|
77
|
-
try {
|
|
78
|
-
return decodeURIComponent(cookieVal);
|
|
79
|
-
} catch {
|
|
80
|
-
return cookieVal;
|
|
81
|
-
}
|
|
100
|
+
getUserHostAddress(): string {
|
|
101
|
+
return this._clientIp || this.baseReq.headers.get('Fastly-Client-IP') || '';
|
|
82
102
|
}
|
|
83
103
|
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
return '';
|
|
104
|
+
getCookieValue(name: string): string | undefined {
|
|
105
|
+
if (!this._parsedCookieDic) {
|
|
106
|
+
this._parsedCookieDic = this._parseCookies(this.getHeader('cookie'));
|
|
88
107
|
}
|
|
89
|
-
const value = this.baseReq.headers.get(name);
|
|
90
|
-
if (value == null) return '';
|
|
91
|
-
return value!;
|
|
92
|
-
}
|
|
93
108
|
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
109
|
+
const cookieValue = this._parsedCookieDic[name];
|
|
110
|
+
if (cookieValue) {
|
|
111
|
+
try {
|
|
112
|
+
return decodeURIComponent(cookieValue);
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
97
116
|
}
|
|
98
|
-
|
|
117
|
+
|
|
118
|
+
return;
|
|
99
119
|
}
|
|
100
120
|
|
|
101
|
-
|
|
102
|
-
return this.
|
|
121
|
+
getRequestBodyAsString(): string {
|
|
122
|
+
return this._bodyString;
|
|
103
123
|
}
|
|
104
124
|
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
private _parseCookies(cookieValue: string): Record<string, string> {
|
|
126
|
+
const parsedCookie: Record<string, string> = {};
|
|
127
|
+
cookieValue.split(';').forEach(function (cookie) {
|
|
128
|
+
if (cookie) {
|
|
129
|
+
const parts = cookie.split('=');
|
|
130
|
+
if (parts.length >= 2) parsedCookie[parts[0].trim()] = parts[1].trim();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return parsedCookie;
|
|
107
134
|
}
|
|
108
135
|
}
|
|
109
136
|
|
|
110
|
-
|
|
137
|
+
class FastlyHttpResponse implements IHttpResponse {
|
|
111
138
|
private readonly headers: Headers;
|
|
112
139
|
|
|
113
140
|
constructor() {
|
|
114
141
|
this.headers = new Headers();
|
|
115
142
|
}
|
|
116
143
|
|
|
117
|
-
setCookie(
|
|
144
|
+
setCookie(
|
|
145
|
+
cookieName: string,
|
|
146
|
+
cookieValue: string,
|
|
147
|
+
domain: string,
|
|
148
|
+
expiration: number,
|
|
149
|
+
httpOnly: boolean,
|
|
150
|
+
isSecure: boolean
|
|
151
|
+
): void {
|
|
118
152
|
const expirationDate = new Date(expiration * 1000);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
153
|
+
|
|
154
|
+
let setCookieString = `${cookieName}=${encodeURIComponent(cookieValue)}; expires=${expirationDate.toUTCString()};`;
|
|
155
|
+
|
|
156
|
+
if (domain) {
|
|
157
|
+
setCookieString += ` domain=${domain};`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (httpOnly) {
|
|
161
|
+
setCookieString += ' HttpOnly;';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (isSecure) {
|
|
165
|
+
setCookieString += ' Secure;';
|
|
122
166
|
}
|
|
123
|
-
|
|
124
|
-
|
|
167
|
+
|
|
168
|
+
setCookieString += ' path=/';
|
|
169
|
+
this.headers.append('set-cookie', setCookieString);
|
|
125
170
|
}
|
|
126
171
|
|
|
127
172
|
getHeaders(): Headers {
|
package/src/helper.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export class QueueITHelper {
|
|
2
|
-
static readonly KUP_VERSION: string = "fastly-
|
|
2
|
+
static readonly KUP_VERSION: string = "fastly-4.4.3";
|
|
3
3
|
|
|
4
4
|
static addKUPlatformVersion(redirectQueueUrl: string): string {
|
|
5
5
|
return redirectQueueUrl + "&kupver=" + QueueITHelper.KUP_VERSION;
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export {IntegrationDetails, IntegrationEndpointProvider, IntegrationEndpointCacheConfig, resolveIntegrationDetails} from "./integrationConfigProvider"
|
|
1
|
+
export {IntegrationDetails, EnqueueTokenProviderFactory, IntegrationEndpointProvider, IntegrationEndpointCacheConfig, resolveIntegrationDetails} from "./integrationConfigProvider"
|
|
2
2
|
export {onQueueITRequest, onQueueITResponse} from "./requestResponseHandler";
|
|
3
3
|
export {RequestLogger} from "./helper";
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { ConfigStore } from "fastly:config-store";
|
|
2
|
+
import { IEnqueueTokenProvider } from "@queue-it/connector-javascript";
|
|
2
3
|
import { RequestLogger } from "./helper";
|
|
3
4
|
|
|
5
|
+
export type EnqueueTokenProviderFactory = (
|
|
6
|
+
customerId: string,
|
|
7
|
+
secretKey: string,
|
|
8
|
+
validityTime: number,
|
|
9
|
+
clientIp: string,
|
|
10
|
+
withKey: boolean
|
|
11
|
+
) => IEnqueueTokenProvider;
|
|
12
|
+
|
|
4
13
|
export async function getIntegrationConfig(
|
|
5
14
|
details: IntegrationDetails,
|
|
6
15
|
endpointProvider: IntegrationEndpointProvider
|
|
@@ -46,7 +55,11 @@ const integrationCustomerId = "customerId",
|
|
|
46
55
|
integrationSecret = "secret",
|
|
47
56
|
integrationQueueItOrigin = "queueItOrigin",
|
|
48
57
|
integrationDictionary = "IntegrationConfiguration",
|
|
49
|
-
workerHost = "workerHost"
|
|
58
|
+
workerHost = "workerHost",
|
|
59
|
+
enqueueTokenEnabledKey = "enqueueTokenEnabled",
|
|
60
|
+
enqueueTokenValidityTimeKey = "enqueueTokenValidityTime",
|
|
61
|
+
enqueueTokenKeyEnabledKey = "enqueueTokenKeyEnabled",
|
|
62
|
+
requestBodyEnabledKey = "requestBodyEnabled";
|
|
50
63
|
|
|
51
64
|
export function resolveIntegrationDetails(): IntegrationDetails | null {
|
|
52
65
|
const dict = new ConfigStore(integrationDictionary);
|
|
@@ -65,12 +78,40 @@ export function resolveIntegrationDetails(): IntegrationDetails | null {
|
|
|
65
78
|
workerHostValue = workerHostVal;
|
|
66
79
|
}
|
|
67
80
|
|
|
81
|
+
let enqueueTokenEnabled = true;
|
|
82
|
+
const enqueueTokenEnabledVal = dict.get(enqueueTokenEnabledKey);
|
|
83
|
+
if (enqueueTokenEnabledVal !== null) {
|
|
84
|
+
enqueueTokenEnabled = enqueueTokenEnabledVal.toLowerCase() === "true";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let enqueueTokenValidityTime = 240000;
|
|
88
|
+
const enqueueTokenValidityTimeVal = dict.get(enqueueTokenValidityTimeKey);
|
|
89
|
+
if (enqueueTokenValidityTimeVal !== null) {
|
|
90
|
+
enqueueTokenValidityTime = parseInt(enqueueTokenValidityTimeVal, 10);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let enqueueTokenKeyEnabled = false;
|
|
94
|
+
const enqueueTokenKeyEnabledVal = dict.get(enqueueTokenKeyEnabledKey);
|
|
95
|
+
if (enqueueTokenKeyEnabledVal !== null) {
|
|
96
|
+
enqueueTokenKeyEnabled = enqueueTokenKeyEnabledVal.toLowerCase() === "true";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let requestBodyEnabled = false;
|
|
100
|
+
const requestBodyEnabledVal = dict.get(requestBodyEnabledKey);
|
|
101
|
+
if (requestBodyEnabledVal !== null) {
|
|
102
|
+
requestBodyEnabled = requestBodyEnabledVal.toLowerCase() === "true";
|
|
103
|
+
}
|
|
104
|
+
|
|
68
105
|
return new IntegrationDetails(
|
|
69
106
|
dict.get(integrationQueueItOrigin)!,
|
|
70
107
|
dict.get(integrationCustomerId)!,
|
|
71
108
|
dict.get(integrationSecret)!,
|
|
72
109
|
dict.get(integrationApiKey)!,
|
|
73
|
-
workerHostValue
|
|
110
|
+
workerHostValue,
|
|
111
|
+
enqueueTokenEnabled,
|
|
112
|
+
enqueueTokenValidityTime,
|
|
113
|
+
enqueueTokenKeyEnabled,
|
|
114
|
+
requestBodyEnabled
|
|
74
115
|
);
|
|
75
116
|
}
|
|
76
117
|
|
|
@@ -117,12 +158,26 @@ class MockLogger implements RequestLogger {
|
|
|
117
158
|
}
|
|
118
159
|
|
|
119
160
|
export class IntegrationDetails {
|
|
161
|
+
public enqueueTokenProviderFactory: EnqueueTokenProviderFactory | null = null;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The client IP address of the end user. In Fastly Compute, the client IP is not available
|
|
165
|
+
* via request headers (unlike Cloudflare's cf-connecting-ip or Akamai's True-Client-IP).
|
|
166
|
+
* Instead, it must be obtained from event.client.address in the fetch event handler and
|
|
167
|
+
* passed here. Used by the SDK for IP-based trigger matching and enqueue token generation.
|
|
168
|
+
*/
|
|
169
|
+
public clientIp: string = '';
|
|
170
|
+
|
|
120
171
|
constructor(
|
|
121
172
|
public queueItOrigin: string,
|
|
122
173
|
public customerId: string,
|
|
123
174
|
public secretKey: string,
|
|
124
175
|
public apiKey: string,
|
|
125
176
|
public workerHost: string,
|
|
177
|
+
public enqueueTokenEnabled: boolean = true,
|
|
178
|
+
public enqueueTokenValidityTime: number = 240000,
|
|
179
|
+
public enqueueTokenKeyEnabled: boolean = false,
|
|
180
|
+
public requestBodyEnabled: boolean = false,
|
|
126
181
|
public provider: IntegrationEndpointProvider = new QueueItIntegrationEndpointProvider(),
|
|
127
182
|
public logger: RequestLogger = new MockLogger()
|
|
128
183
|
) {}
|
|
@@ -8,31 +8,11 @@ import {
|
|
|
8
8
|
QueueItIntegrationEndpointProvider,
|
|
9
9
|
} from "./integrationConfigProvider";
|
|
10
10
|
|
|
11
|
-
function getParameterByName(url: string, name: string): string {
|
|
12
|
-
name = name.replace(/[\[\]]/g, '\\$&');
|
|
13
|
-
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
|
|
14
|
-
const results = regex.exec(url);
|
|
15
|
-
if (!results) return '';
|
|
16
|
-
if (!results[2]) return '';
|
|
17
|
-
return results[2].replace(/\+/g, ' ');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function removeQueueItToken(url: string): string {
|
|
21
|
-
const pattern = new RegExp("([?&])(" + KnownUser.QueueITTokenKey + "=[^&]*)", 'gi');
|
|
22
|
-
const match = pattern.exec(url);
|
|
23
|
-
if (match === null) return url;
|
|
24
|
-
url = url.replaceAll(match[0], '');
|
|
25
|
-
return url;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function addNoCacheHeaders(res: Response): void {
|
|
29
|
-
res.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0');
|
|
30
|
-
res.headers.set('Pragma', 'no-cache');
|
|
31
|
-
res.headers.set('Expires', 'Fri, 01 Jan 1990 00:00:00 GMT');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
11
|
const QUEUEIT_FAILED_HEADERNAME = "x-queueit-failed";
|
|
12
|
+
const QUEUEIT_CONNECTOR_EXECUTED_HEADER_NAME = "x-queueit-connector";
|
|
13
|
+
|
|
35
14
|
let httpProvider: FastlyHttpContextProvider | null = null;
|
|
15
|
+
let sendNoCacheHeaders: boolean = false;
|
|
36
16
|
|
|
37
17
|
export async function onQueueITRequest(
|
|
38
18
|
req: Request,
|
|
@@ -52,18 +32,52 @@ export async function onQueueITRequest(
|
|
|
52
32
|
conf.provider == null
|
|
53
33
|
? new QueueItIntegrationEndpointProvider()
|
|
54
34
|
: conf.provider;
|
|
55
|
-
httpProvider = getHttpHandler(req);
|
|
56
35
|
|
|
57
|
-
let
|
|
58
|
-
|
|
36
|
+
let bodyString = '';
|
|
37
|
+
if (conf.requestBodyEnabled) {
|
|
38
|
+
bodyString = ((await req.clone().text()) || '').substring(0, 2048);
|
|
39
|
+
}
|
|
59
40
|
|
|
60
|
-
|
|
61
|
-
|
|
41
|
+
httpProvider = getHttpHandler(req, conf.clientIp, bodyString);
|
|
42
|
+
sendNoCacheHeaders = false;
|
|
43
|
+
|
|
44
|
+
if (conf.enqueueTokenEnabled) {
|
|
45
|
+
const clientIp = httpProvider.getHttpRequest().getUserHostAddress();
|
|
46
|
+
if (conf.enqueueTokenProviderFactory) {
|
|
47
|
+
httpProvider.setCustomEnqueueTokenProvider(
|
|
48
|
+
conf.enqueueTokenProviderFactory(
|
|
49
|
+
conf.customerId,
|
|
50
|
+
conf.secretKey,
|
|
51
|
+
conf.enqueueTokenValidityTime,
|
|
52
|
+
clientIp,
|
|
53
|
+
conf.enqueueTokenKeyEnabled
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
httpProvider.setEnqueueTokenProvider(
|
|
58
|
+
conf.customerId,
|
|
59
|
+
conf.secretKey,
|
|
60
|
+
conf.enqueueTokenValidityTime,
|
|
61
|
+
clientIp,
|
|
62
|
+
conf.enqueueTokenKeyEnabled
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
62
66
|
|
|
63
|
-
// The requestUrlWithoutToken is used to match Triggers and as the Target url (where to return the users to).
|
|
64
|
-
// It is therefor important that this is exactly the url of the users browsers. So, if your webserver is
|
|
65
|
-
// behind e.g. a load balancer that modifies the host name or port, reformat requestUrlWithoutToken before proceeding.
|
|
66
67
|
try {
|
|
68
|
+
const integrationConfigJson = await getIntegrationConfig(conf, integrationProvider);
|
|
69
|
+
const requestUrl: string = conf.resolveWorkerRequestUrl(req.url);
|
|
70
|
+
|
|
71
|
+
const queueItToken = getQueueItToken(requestUrl, httpProvider);
|
|
72
|
+
const requestUrlWithoutToken = requestUrl.replace(
|
|
73
|
+
new RegExp("([?&])(" + KnownUser.QueueITTokenKey + "=[^&]*)", "i"),
|
|
74
|
+
""
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// The requestUrlWithoutToken is used to match Triggers and as the Target url (where to return the users to).
|
|
78
|
+
// It is therefor important that this is exactly the url of the users browsers. So, if your webserver is
|
|
79
|
+
// behind e.g. a load balancer that modifies the host name or port, reformat requestUrlWithoutToken before proceeding.
|
|
80
|
+
|
|
67
81
|
const validationResult = await KnownUser.validateRequestByIntegrationConfig(
|
|
68
82
|
requestUrlWithoutToken,
|
|
69
83
|
queueItToken,
|
|
@@ -75,22 +89,23 @@ export async function onQueueITRequest(
|
|
|
75
89
|
|
|
76
90
|
if (validationResult.doRedirect()) {
|
|
77
91
|
if (validationResult.isAjaxResult) {
|
|
78
|
-
|
|
92
|
+
const response = new Response(null, {
|
|
79
93
|
status: 200,
|
|
80
94
|
headers: httpProvider!.getResponseHeaders(),
|
|
81
95
|
});
|
|
96
|
+
const headerKey = validationResult.getAjaxQueueRedirectHeaderKey();
|
|
97
|
+
const queueRedirectUrl = validationResult.getAjaxRedirectUrl();
|
|
98
|
+
|
|
82
99
|
// In case of ajax call send the user to the queue by sending a custom queue-it header and redirecting user to queue from javascript
|
|
83
|
-
response.headers.set("Access-Control-Expose-Headers", validationResult.getAjaxQueueRedirectHeaderKey());
|
|
84
100
|
response.headers.set(
|
|
85
|
-
|
|
86
|
-
QueueITHelper.addKUPlatformVersion(
|
|
87
|
-
validationResult.getAjaxRedirectUrl()
|
|
88
|
-
)
|
|
101
|
+
headerKey,
|
|
102
|
+
QueueITHelper.addKUPlatformVersion(queueRedirectUrl)
|
|
89
103
|
);
|
|
90
|
-
|
|
104
|
+
response.headers.set("Access-Control-Expose-Headers", headerKey);
|
|
105
|
+
sendNoCacheHeaders = true;
|
|
91
106
|
return response;
|
|
92
107
|
} else {
|
|
93
|
-
|
|
108
|
+
const response = new Response(null, {
|
|
94
109
|
status: 302,
|
|
95
110
|
headers: httpProvider!.getResponseHeaders(),
|
|
96
111
|
});
|
|
@@ -99,47 +114,87 @@ export async function onQueueITRequest(
|
|
|
99
114
|
"Location",
|
|
100
115
|
QueueITHelper.addKUPlatformVersion(validationResult.redirectUrl)
|
|
101
116
|
);
|
|
102
|
-
|
|
117
|
+
sendNoCacheHeaders = true;
|
|
103
118
|
return response;
|
|
104
119
|
}
|
|
105
120
|
} else {
|
|
106
121
|
// Request can continue - we remove queueittoken from querystring parameter to avoid sharing of user specific token
|
|
107
|
-
// Support mobile scenario adding the condition !validationResult.isAjaxResult
|
|
108
122
|
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
validationResult.actionType == "Queue"
|
|
123
|
+
requestUrl !== requestUrlWithoutToken &&
|
|
124
|
+
validationResult.actionType === "Queue"
|
|
112
125
|
) {
|
|
113
|
-
|
|
126
|
+
const response = new Response(null, {
|
|
114
127
|
status: 302,
|
|
115
128
|
headers: httpProvider!.getResponseHeaders(),
|
|
116
129
|
});
|
|
117
130
|
response.headers.set("Location", requestUrlWithoutToken);
|
|
118
|
-
|
|
131
|
+
sendNoCacheHeaders = true;
|
|
119
132
|
return response;
|
|
120
133
|
} else {
|
|
121
134
|
// lets caller decide the next step, or just serve the request normally
|
|
122
135
|
return null;
|
|
123
136
|
}
|
|
124
137
|
}
|
|
125
|
-
} catch {
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (console && console.log) {
|
|
140
|
+
console.log("ERROR:" + e);
|
|
141
|
+
}
|
|
126
142
|
httpProvider!.isError = true;
|
|
143
|
+
return null;
|
|
127
144
|
}
|
|
128
|
-
|
|
129
|
-
return null;
|
|
130
145
|
}
|
|
131
146
|
|
|
132
|
-
//Fill in the Queue-it headers
|
|
147
|
+
// Fill in the Queue-it headers
|
|
133
148
|
export function onQueueITResponse(res: Response): void {
|
|
134
|
-
|
|
149
|
+
res.headers.set(QUEUEIT_CONNECTOR_EXECUTED_HEADER_NAME, "fastly");
|
|
150
|
+
|
|
151
|
+
if (httpProvider) {
|
|
152
|
+
const contextHeaders = httpProvider.getResponseHeaders();
|
|
153
|
+
for (const key of contextHeaders.keys()) {
|
|
154
|
+
if (key.length == 0) continue;
|
|
155
|
+
const value = contextHeaders.get(key);
|
|
156
|
+
if (value != null && value.length > 0)
|
|
157
|
+
res.headers.append(key, value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (httpProvider.isError) {
|
|
161
|
+
res.headers.append(QUEUEIT_FAILED_HEADERNAME, "true");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (sendNoCacheHeaders) {
|
|
166
|
+
addNoCacheHeaders(res);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function addNoCacheHeaders(res: Response): void {
|
|
171
|
+
res.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0');
|
|
172
|
+
res.headers.set('Pragma', 'no-cache');
|
|
173
|
+
res.headers.set('Expires', 'Fri, 01 Jan 1990 00:00:00 GMT');
|
|
174
|
+
}
|
|
135
175
|
|
|
136
|
-
|
|
137
|
-
|
|
176
|
+
function getQueueItToken(
|
|
177
|
+
requestUrl: string,
|
|
178
|
+
httpContext: FastlyHttpContextProvider
|
|
179
|
+
): string {
|
|
180
|
+
const queueItToken = getParameterByName(requestUrl, KnownUser.QueueITTokenKey);
|
|
181
|
+
if (queueItToken) {
|
|
182
|
+
return queueItToken;
|
|
138
183
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
184
|
+
|
|
185
|
+
const tokenHeaderName = `x-${KnownUser.QueueITTokenKey}`;
|
|
186
|
+
return httpContext.getHttpRequest().getHeader(tokenHeaderName);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getParameterByName(url: string, name: string): string {
|
|
190
|
+
name = name.replace(/[\[\]]/g, '\\$&');
|
|
191
|
+
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
|
|
192
|
+
const results = regex.exec(url);
|
|
193
|
+
if (!results) return '';
|
|
194
|
+
if (!results[2]) return '';
|
|
195
|
+
try {
|
|
196
|
+
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
|
197
|
+
} catch {
|
|
198
|
+
return results[2];
|
|
144
199
|
}
|
|
145
200
|
}
|