@browserless.io/browserless 2.8.0 → 2.10.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/CHANGELOG.md +13 -1
- package/README.md +41 -3
- package/assets/debugger.png +0 -0
- package/bin/scaffold/src/hello-world.http.ts +3 -2
- package/build/browserless.d.ts +5 -4
- package/build/browserless.js +19 -13
- package/build/browsers/chrome.cdp.d.ts +2 -2
- package/build/browsers/chrome.cdp.js +2 -2
- package/build/browsers/chrome.playwright.d.ts +2 -2
- package/build/browsers/chrome.playwright.js +2 -2
- package/build/browsers/chromium.cdp.d.ts +4 -4
- package/build/browsers/chromium.cdp.js +49 -32
- package/build/browsers/chromium.playwright.d.ts +4 -4
- package/build/browsers/chromium.playwright.js +14 -13
- package/build/browsers/firefox.playwright.d.ts +4 -4
- package/build/browsers/firefox.playwright.js +14 -13
- package/build/browsers/index.d.ts +24 -8
- package/build/browsers/index.js +20 -15
- package/build/browsers/webkit.playwright.d.ts +4 -4
- package/build/browsers/webkit.playwright.js +14 -13
- package/build/config.d.ts +3 -0
- package/build/config.js +3 -0
- package/build/file-system.d.ts +2 -3
- package/build/file-system.js +2 -2
- package/build/http.d.ts +1 -0
- package/build/http.js +1 -0
- package/build/index.js +7 -7
- package/build/limiter.d.ts +2 -3
- package/build/limiter.js +11 -11
- package/build/logger.d.ts +16 -9
- package/build/logger.js +32 -16
- package/build/monitoring.d.ts +2 -3
- package/build/monitoring.js +4 -4
- package/build/router.d.ts +1 -3
- package/build/router.js +21 -22
- package/build/routes/chrome/http/content.post.body.json +8 -8
- package/build/routes/chrome/http/pdf.post.body.json +8 -8
- package/build/routes/chrome/http/scrape.post.body.json +8 -8
- package/build/routes/chrome/http/screenshot.post.body.json +8 -8
- package/build/routes/chromium/http/content.post.body.json +8 -8
- package/build/routes/chromium/http/pdf.post.body.json +8 -8
- package/build/routes/chromium/http/scrape.post.body.json +8 -8
- package/build/routes/chromium/http/screenshot.post.body.json +8 -8
- package/build/routes/management/http/pressure.get.d.ts +63 -0
- package/build/routes/management/http/pressure.get.js +56 -0
- package/build/routes/management/http/pressure.get.response.json +76 -0
- package/build/routes/management/http/static.get.js +3 -3
- package/build/routes/management/tests/management.spec.js +16 -0
- package/build/server.d.ts +1 -3
- package/build/server.js +33 -30
- package/build/shared/content.http.d.ts +1 -1
- package/build/shared/content.http.js +4 -1
- package/build/shared/download.http.js +9 -9
- package/build/shared/function.http.js +2 -2
- package/build/shared/json-protocol.http.d.ts +3 -3
- package/build/shared/json-protocol.http.js +2 -2
- package/build/shared/json-version.http.d.ts +3 -3
- package/build/shared/json-version.http.js +2 -2
- package/build/shared/pdf.http.d.ts +1 -1
- package/build/shared/pdf.http.js +6 -4
- package/build/shared/performance.http.js +1 -0
- package/build/shared/scrape.http.d.ts +1 -1
- package/build/shared/scrape.http.js +4 -1
- package/build/shared/screenshot.http.d.ts +1 -1
- package/build/shared/screenshot.http.js +4 -1
- package/build/shared/utils/function/handler.js +7 -7
- package/build/shared/utils/performance/child.js +4 -4
- package/build/shared/utils/performance/main.d.ts +1 -1
- package/build/shared/utils/performance/main.js +5 -7
- package/build/shared/utils/performance/types.d.ts +2 -1
- package/build/types.d.ts +10 -1
- package/build/types.js +10 -1
- package/build/utils.d.ts +1 -1
- package/build/utils.js +6 -2
- package/package.json +18 -15
- package/scripts/install-debugger.js +55 -15
- package/src/browserless.ts +25 -12
- package/src/browsers/chrome.cdp.ts +2 -5
- package/src/browsers/chrome.playwright.ts +2 -5
- package/src/browsers/chromium.cdp.ts +84 -35
- package/src/browsers/chromium.playwright.ts +26 -13
- package/src/browsers/firefox.playwright.ts +28 -13
- package/src/browsers/index.ts +24 -16
- package/src/browsers/webkit.playwright.ts +28 -13
- package/src/config.ts +4 -0
- package/src/file-system.ts +2 -7
- package/src/http.ts +1 -0
- package/src/index.ts +7 -7
- package/src/limiter.ts +13 -11
- package/src/logger.ts +39 -18
- package/src/monitoring.ts +6 -8
- package/src/router.ts +20 -20
- package/src/routes/management/http/pressure.get.ts +135 -0
- package/src/routes/management/http/static.get.ts +3 -5
- package/src/routes/management/tests/management.spec.ts +26 -0
- package/src/server.ts +43 -30
- package/src/shared/content.http.ts +5 -1
- package/src/shared/download.http.ts +9 -9
- package/src/shared/function.http.ts +2 -2
- package/src/shared/json-protocol.http.ts +8 -3
- package/src/shared/json-version.http.ts +8 -4
- package/src/shared/pdf.http.ts +6 -4
- package/src/shared/performance.http.ts +1 -0
- package/src/shared/scrape.http.ts +5 -1
- package/src/shared/screenshot.http.ts +4 -1
- package/src/shared/utils/function/handler.ts +7 -7
- package/src/shared/utils/performance/child.ts +4 -4
- package/src/shared/utils/performance/main.ts +5 -6
- package/src/shared/utils/performance/types.ts +2 -1
- package/src/types.ts +9 -0
- package/src/utils.ts +7 -2
- package/static/docs/swagger.json +138 -10
- package/static/docs/swagger.min.json +137 -9
- package/static/function/client.js +4290 -5542
- package/static/function/index.html +4290 -5542
package/src/monitoring.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Config,
|
|
3
|
-
IResourceLoad,
|
|
4
|
-
createLogger,
|
|
5
|
-
} from '@browserless.io/browserless';
|
|
1
|
+
import { Config, IResourceLoad, Logger } from '@browserless.io/browserless';
|
|
6
2
|
import { EventEmitter } from 'events';
|
|
7
3
|
import si from 'systeminformation';
|
|
8
4
|
|
|
9
5
|
export class Monitoring extends EventEmitter {
|
|
10
|
-
protected log =
|
|
6
|
+
protected log = new Logger('hardware');
|
|
11
7
|
constructor(protected config: Config) {
|
|
12
8
|
super();
|
|
13
9
|
}
|
|
@@ -17,7 +13,7 @@ export class Monitoring extends EventEmitter {
|
|
|
17
13
|
si.currentLoad(),
|
|
18
14
|
si.mem(),
|
|
19
15
|
]).catch((err) => {
|
|
20
|
-
this.log(`Error checking machine stats`, err);
|
|
16
|
+
this.log.error(`Error checking machine stats`, err);
|
|
21
17
|
return [null, null];
|
|
22
18
|
});
|
|
23
19
|
|
|
@@ -40,7 +36,9 @@ export class Monitoring extends EventEmitter {
|
|
|
40
36
|
const cpuInt = cpu && Math.ceil(cpu * 100);
|
|
41
37
|
const memoryInt = memory && Math.ceil(memory * 100);
|
|
42
38
|
|
|
43
|
-
this.log(
|
|
39
|
+
this.log.info(
|
|
40
|
+
`Checking overload status: CPU ${cpuInt}% Memory ${memoryInt}%`,
|
|
41
|
+
);
|
|
44
42
|
|
|
45
43
|
const cpuOverloaded = !!(cpuInt && cpuInt >= this.config.getCPULimit());
|
|
46
44
|
const memoryOverloaded = !!(
|
package/src/router.ts
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
Response,
|
|
14
14
|
WebSocketRoute,
|
|
15
15
|
contentTypes,
|
|
16
|
-
createLogger,
|
|
17
16
|
isConnected,
|
|
18
17
|
writeResponse,
|
|
19
18
|
} from '@browserless.io/browserless';
|
|
@@ -22,8 +21,7 @@ import micromatch from 'micromatch';
|
|
|
22
21
|
import stream from 'stream';
|
|
23
22
|
|
|
24
23
|
export class Router extends EventEmitter {
|
|
25
|
-
protected log =
|
|
26
|
-
protected verbose = createLogger('router:verbose');
|
|
24
|
+
protected log = new Logger('router');
|
|
27
25
|
protected httpRoutes: Array<HTTPRoute | BrowserHTTPRoute> = [];
|
|
28
26
|
protected webSocketRoutes: Array<WebSocketRoute | BrowserWebsocketRoute> = [];
|
|
29
27
|
|
|
@@ -43,22 +41,22 @@ export class Router extends EventEmitter {
|
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
protected onQueueFullHTTP = (_req: Request, res: Response) => {
|
|
46
|
-
this.log(`Queue is full, sending 429 response`);
|
|
44
|
+
this.log.warn(`Queue is full, sending 429 response`);
|
|
47
45
|
return writeResponse(res, 429, 'Too many requests');
|
|
48
46
|
};
|
|
49
47
|
|
|
50
48
|
protected onQueueFullWebSocket = (_req: Request, socket: stream.Duplex) => {
|
|
51
|
-
this.log(`Queue is full, sending 429 response`);
|
|
49
|
+
this.log.warn(`Queue is full, sending 429 response`);
|
|
52
50
|
return writeResponse(socket, 429, 'Too many requests');
|
|
53
51
|
};
|
|
54
52
|
|
|
55
53
|
protected onHTTPTimeout = (_req: Request, res: Response) => {
|
|
56
|
-
this.log(`HTTP job has timedout, sending 429 response`);
|
|
54
|
+
this.log.error(`HTTP job has timedout, sending 429 response`);
|
|
57
55
|
return writeResponse(res, 408, 'Request has timed out');
|
|
58
56
|
};
|
|
59
57
|
|
|
60
58
|
protected onWebsocketTimeout = (_req: Request, socket: stream.Duplex) => {
|
|
61
|
-
this.log(`Websocket job has timedout, sending 429 response`);
|
|
59
|
+
this.log.error(`Websocket job has timedout, sending 429 response`);
|
|
62
60
|
return writeResponse(socket, 408, 'Request has timed out');
|
|
63
61
|
};
|
|
64
62
|
|
|
@@ -69,7 +67,7 @@ export class Router extends EventEmitter {
|
|
|
69
67
|
) =>
|
|
70
68
|
async (req: Request, res: Response) => {
|
|
71
69
|
if (!isConnected(res)) {
|
|
72
|
-
this.log(`HTTP Request has closed prior to running`);
|
|
70
|
+
this.log.warn(`HTTP Request has closed prior to running`);
|
|
73
71
|
return Promise.resolve();
|
|
74
72
|
}
|
|
75
73
|
const logger = new this.logger(route.name, req);
|
|
@@ -77,10 +75,11 @@ export class Router extends EventEmitter {
|
|
|
77
75
|
const browser = await this.browserManager.getBrowserForRequest(
|
|
78
76
|
req,
|
|
79
77
|
route,
|
|
78
|
+
logger,
|
|
80
79
|
);
|
|
81
80
|
|
|
82
81
|
if (!isConnected(res)) {
|
|
83
|
-
this.log(`HTTP Request has closed prior to running`);
|
|
82
|
+
this.log.warn(`HTTP Request has closed prior to running`);
|
|
84
83
|
this.browserManager.complete(browser);
|
|
85
84
|
return Promise.resolve();
|
|
86
85
|
}
|
|
@@ -90,7 +89,7 @@ export class Router extends EventEmitter {
|
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
try {
|
|
93
|
-
this.
|
|
92
|
+
this.log.trace(`Running found HTTP handler.`);
|
|
94
93
|
return await Promise.race([
|
|
95
94
|
handler(req, res, logger, browser),
|
|
96
95
|
new Promise((resolve, reject) => {
|
|
@@ -98,13 +97,13 @@ export class Router extends EventEmitter {
|
|
|
98
97
|
if (!res.writableEnded) {
|
|
99
98
|
reject(new Error(`Request closed prior to writing results`));
|
|
100
99
|
}
|
|
101
|
-
this.
|
|
100
|
+
this.log.trace(`Response has been written, resolving`);
|
|
102
101
|
resolve(null);
|
|
103
102
|
});
|
|
104
103
|
}),
|
|
105
104
|
]);
|
|
106
105
|
} finally {
|
|
107
|
-
this.
|
|
106
|
+
this.log.trace(`HTTP Request handler has finished.`);
|
|
108
107
|
this.browserManager.complete(browser);
|
|
109
108
|
}
|
|
110
109
|
}
|
|
@@ -119,7 +118,7 @@ export class Router extends EventEmitter {
|
|
|
119
118
|
) =>
|
|
120
119
|
async (req: Request, socket: stream.Duplex, head: Buffer) => {
|
|
121
120
|
if (!isConnected(socket)) {
|
|
122
|
-
this.log(`WebSocket Request has closed prior to running`);
|
|
121
|
+
this.log.warn(`WebSocket Request has closed prior to running`);
|
|
123
122
|
return Promise.resolve();
|
|
124
123
|
}
|
|
125
124
|
const logger = new this.logger(route.name, req);
|
|
@@ -127,10 +126,11 @@ export class Router extends EventEmitter {
|
|
|
127
126
|
const browser = await this.browserManager.getBrowserForRequest(
|
|
128
127
|
req,
|
|
129
128
|
route,
|
|
129
|
+
logger,
|
|
130
130
|
);
|
|
131
131
|
|
|
132
132
|
if (!isConnected(socket)) {
|
|
133
|
-
this.log(`WebSocket Request has closed prior to running`);
|
|
133
|
+
this.log.warn(`WebSocket Request has closed prior to running`);
|
|
134
134
|
this.browserManager.complete(browser);
|
|
135
135
|
return Promise.resolve();
|
|
136
136
|
}
|
|
@@ -140,10 +140,10 @@ export class Router extends EventEmitter {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
try {
|
|
143
|
-
this.
|
|
143
|
+
this.log.trace(`Running found WebSocket handler.`);
|
|
144
144
|
await handler(req, socket, head, logger, browser);
|
|
145
145
|
} finally {
|
|
146
|
-
this.
|
|
146
|
+
this.log.trace(`WebSocket Request handler has finished.`);
|
|
147
147
|
this.browserManager.complete(browser);
|
|
148
148
|
}
|
|
149
149
|
return;
|
|
@@ -154,7 +154,7 @@ export class Router extends EventEmitter {
|
|
|
154
154
|
public registerHTTPRoute(
|
|
155
155
|
route: HTTPRoute | BrowserHTTPRoute,
|
|
156
156
|
): HTTPRoute | BrowserHTTPRoute {
|
|
157
|
-
this.
|
|
157
|
+
this.log.trace(
|
|
158
158
|
`Registering HTTP ${route.method.toUpperCase()} ${route.path}`,
|
|
159
159
|
);
|
|
160
160
|
|
|
@@ -176,7 +176,7 @@ export class Router extends EventEmitter {
|
|
|
176
176
|
);
|
|
177
177
|
|
|
178
178
|
if (duplicatePaths.length) {
|
|
179
|
-
this.log(`Found duplicate routes: ${duplicatePaths.join(', ')}`);
|
|
179
|
+
this.log.warn(`Found duplicate routes: ${duplicatePaths.join(', ')}`);
|
|
180
180
|
}
|
|
181
181
|
this.httpRoutes.push(route);
|
|
182
182
|
|
|
@@ -186,7 +186,7 @@ export class Router extends EventEmitter {
|
|
|
186
186
|
public registerWebSocketRoute(
|
|
187
187
|
route: WebSocketRoute | BrowserWebsocketRoute,
|
|
188
188
|
): WebSocketRoute | BrowserWebsocketRoute {
|
|
189
|
-
this.
|
|
189
|
+
this.log.trace(`Registering WebSocket "${route.path}"`);
|
|
190
190
|
|
|
191
191
|
const bound = route.handler.bind(route);
|
|
192
192
|
const wrapped = this.wrapWebSocketHandler(route, bound);
|
|
@@ -206,7 +206,7 @@ export class Router extends EventEmitter {
|
|
|
206
206
|
);
|
|
207
207
|
|
|
208
208
|
if (duplicatePaths.length) {
|
|
209
|
-
this.log(`Found duplicate routes: ${duplicatePaths.join(', ')}`);
|
|
209
|
+
this.log.warn(`Found duplicate routes: ${duplicatePaths.join(', ')}`);
|
|
210
210
|
}
|
|
211
211
|
this.webSocketRoutes.push(route);
|
|
212
212
|
return route;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
APITags,
|
|
3
|
+
BrowserlessRoutes,
|
|
4
|
+
HTTPManagementRoutes,
|
|
5
|
+
HTTPRoute,
|
|
6
|
+
Methods,
|
|
7
|
+
Request,
|
|
8
|
+
contentTypes,
|
|
9
|
+
jsonResponse,
|
|
10
|
+
} from '@browserless.io/browserless';
|
|
11
|
+
import { ServerResponse } from 'http';
|
|
12
|
+
|
|
13
|
+
export type ResponseSchema = {
|
|
14
|
+
/**
|
|
15
|
+
* An integer representing the percentage of CPU being used. For instance 92 means 92%
|
|
16
|
+
*/
|
|
17
|
+
cpu: number | null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A number of milliseconds since epoch, or "Date.now()" equivalent.
|
|
21
|
+
*/
|
|
22
|
+
date: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether or not a session can be connected and immediately ran on a health instance.
|
|
26
|
+
*/
|
|
27
|
+
isAvailable: boolean;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The maximum amount of browsers that can be ran at a single time.
|
|
31
|
+
*/
|
|
32
|
+
maxConcurrent: number;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The maximum amount of queued connections allowed at a single time.
|
|
36
|
+
*/
|
|
37
|
+
maxQueued: number;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* An integer representing the percentage of Memory being used. For instance 95 means 95%
|
|
41
|
+
*/
|
|
42
|
+
memory: number | null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A human-readable message as the overall status of the instance.
|
|
46
|
+
*/
|
|
47
|
+
message: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The current number of connect or API calls pending to run.
|
|
51
|
+
*/
|
|
52
|
+
queued: number;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A simple single-word reason as to why an instance may or may not be available.
|
|
56
|
+
*/
|
|
57
|
+
reason: 'full' | 'cpu' | 'memory' | '';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The number of recent connections that were rejected due to the queue and concurrency
|
|
61
|
+
* limits having been filled.
|
|
62
|
+
*/
|
|
63
|
+
recentlyRejected: number;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The current number of running connections or API calls.
|
|
67
|
+
*/
|
|
68
|
+
running: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default class PressureGetRoute extends HTTPRoute {
|
|
72
|
+
name = BrowserlessRoutes.PressureGetRoute;
|
|
73
|
+
accepts = [contentTypes.any];
|
|
74
|
+
auth = true;
|
|
75
|
+
browser = null;
|
|
76
|
+
concurrency = false;
|
|
77
|
+
contentTypes = [contentTypes.json];
|
|
78
|
+
description = `Returns a JSON body of stats related to the pressure being created on the instance.`;
|
|
79
|
+
method = Methods.get;
|
|
80
|
+
path = HTTPManagementRoutes.pressure;
|
|
81
|
+
tags = [APITags.management];
|
|
82
|
+
handler = async (_req: Request, res: ServerResponse): Promise<void> => {
|
|
83
|
+
const monitoring = this.monitoring();
|
|
84
|
+
const config = this.config();
|
|
85
|
+
const limiter = this.limiter();
|
|
86
|
+
const metrics = this.metrics();
|
|
87
|
+
|
|
88
|
+
const {
|
|
89
|
+
cpuInt: cpu,
|
|
90
|
+
memoryInt: memory,
|
|
91
|
+
cpuOverloaded,
|
|
92
|
+
memoryOverloaded,
|
|
93
|
+
} = await monitoring.overloaded();
|
|
94
|
+
const date = Date.now();
|
|
95
|
+
const hasCapacity = limiter.hasCapacity;
|
|
96
|
+
const queued = limiter.waiting;
|
|
97
|
+
const isAvailable = hasCapacity && !cpuOverloaded && !memoryOverloaded;
|
|
98
|
+
const running = limiter.executing;
|
|
99
|
+
const recentlyRejected = metrics.get().rejected;
|
|
100
|
+
const maxConcurrent = config.getConcurrent();
|
|
101
|
+
const maxQueued = config.getQueued();
|
|
102
|
+
|
|
103
|
+
const reason = !hasCapacity
|
|
104
|
+
? 'full'
|
|
105
|
+
: cpuOverloaded
|
|
106
|
+
? 'cpu'
|
|
107
|
+
: memoryOverloaded
|
|
108
|
+
? 'memory'
|
|
109
|
+
: '';
|
|
110
|
+
|
|
111
|
+
const message = !hasCapacity
|
|
112
|
+
? 'Concurrency and queue are full'
|
|
113
|
+
: cpuOverloaded
|
|
114
|
+
? 'CPU is over the configured maximum for cpu percent'
|
|
115
|
+
: memoryOverloaded
|
|
116
|
+
? 'Memory is over the configured maximum for memory percent'
|
|
117
|
+
: '';
|
|
118
|
+
|
|
119
|
+
const response: ResponseSchema = {
|
|
120
|
+
cpu,
|
|
121
|
+
date,
|
|
122
|
+
isAvailable,
|
|
123
|
+
maxConcurrent,
|
|
124
|
+
maxQueued,
|
|
125
|
+
memory,
|
|
126
|
+
message,
|
|
127
|
+
queued,
|
|
128
|
+
reason,
|
|
129
|
+
recentlyRejected,
|
|
130
|
+
running,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return jsonResponse(res, 200, response);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -31,7 +31,7 @@ const streamFile = (
|
|
|
31
31
|
) =>
|
|
32
32
|
new Promise((resolve, reject) => {
|
|
33
33
|
if (contentType) {
|
|
34
|
-
logger.
|
|
34
|
+
logger.debug(`Setting content-type ${contentType}`);
|
|
35
35
|
res.setHeader('Content-Type', contentType);
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -96,15 +96,13 @@ export default class StaticGetRoute extends HTTPRoute {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
if (foundFilePaths.length > 1) {
|
|
99
|
-
logger.
|
|
99
|
+
logger.warn(
|
|
100
100
|
`Multiple files found for request to "${pathname}". Only the first file is served, so please name your files uniquely.`,
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
const [foundFilePath] = foundFilePaths;
|
|
105
|
-
logger.
|
|
106
|
-
`Found new file "${foundFilePath}", caching path and serving`,
|
|
107
|
-
);
|
|
105
|
+
logger.info(`Found new file "${foundFilePath}", caching path and serving`);
|
|
108
106
|
|
|
109
107
|
const contentType = mimeTypes.get(path.extname(foundFilePath));
|
|
110
108
|
|
|
@@ -56,6 +56,19 @@ describe('Management APIs', function () {
|
|
|
56
56
|
);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
it('allows requests to /pressure', async () => {
|
|
60
|
+
await start();
|
|
61
|
+
|
|
62
|
+
await fetch('http://localhost:3000/pressure?token=6R0W53R135510').then(
|
|
63
|
+
async (res) => {
|
|
64
|
+
expect(res.headers.get('content-type')).to.equal(
|
|
65
|
+
'application/json; charset=UTF-8',
|
|
66
|
+
);
|
|
67
|
+
expect(res.status).to.equal(200);
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
59
72
|
it('allows requests to /sessions', async () => {
|
|
60
73
|
await start();
|
|
61
74
|
|
|
@@ -81,4 +94,17 @@ describe('Management APIs', function () {
|
|
|
81
94
|
},
|
|
82
95
|
);
|
|
83
96
|
});
|
|
97
|
+
|
|
98
|
+
it('allows HEAD requests to /active', async () => {
|
|
99
|
+
await start();
|
|
100
|
+
|
|
101
|
+
await fetch('http://localhost:3000/active?token=6R0W53R135510', {
|
|
102
|
+
method: 'HEAD',
|
|
103
|
+
}).then(async (res) => {
|
|
104
|
+
expect(res.headers.get('content-type')).to.equal(
|
|
105
|
+
'text/plain; charset=UTF-8',
|
|
106
|
+
);
|
|
107
|
+
expect(res.status).to.equal(204);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
84
110
|
});
|
package/src/server.ts
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
WebSocketRoute,
|
|
19
19
|
contentTypes,
|
|
20
20
|
convertPathToURL,
|
|
21
|
-
createLogger,
|
|
22
21
|
queryParamsToObject,
|
|
23
22
|
readBody,
|
|
24
23
|
shimLegacyRequests,
|
|
@@ -41,8 +40,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
41
40
|
protected server: http.Server = http.createServer();
|
|
42
41
|
protected port: number;
|
|
43
42
|
protected host?: string;
|
|
44
|
-
protected
|
|
45
|
-
protected verbose = createLogger('server:verbose');
|
|
43
|
+
protected logger = new BlessLogger('server');
|
|
46
44
|
|
|
47
45
|
constructor(
|
|
48
46
|
protected config: Config,
|
|
@@ -56,7 +54,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
56
54
|
this.host = config.getHost();
|
|
57
55
|
this.port = config.getPort();
|
|
58
56
|
|
|
59
|
-
this.
|
|
57
|
+
this.logger.info(
|
|
60
58
|
`Server instantiated with host "${this.host}" on port "${
|
|
61
59
|
this.port
|
|
62
60
|
}" using token "${this.config.getToken()}"`,
|
|
@@ -64,7 +62,9 @@ export class HTTPServer extends EventEmitter {
|
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
protected onHTTPUnauthorized = (_req: Request, res: Response) => {
|
|
67
|
-
this.
|
|
65
|
+
this.logger.error(
|
|
66
|
+
`HTTP request is not properly authorized, responding with 401`,
|
|
67
|
+
);
|
|
68
68
|
this.metrics.addUnauthorized();
|
|
69
69
|
return writeResponse(res, 401, 'Bad or missing authentication.');
|
|
70
70
|
};
|
|
@@ -73,7 +73,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
73
73
|
_req: Request,
|
|
74
74
|
socket: stream.Duplex,
|
|
75
75
|
) => {
|
|
76
|
-
this.
|
|
76
|
+
this.logger.error(
|
|
77
77
|
`Websocket request is not properly authorized, responding with 401`,
|
|
78
78
|
);
|
|
79
79
|
this.metrics.addUnauthorized();
|
|
@@ -81,7 +81,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
public async start(): Promise<void> {
|
|
84
|
-
this.
|
|
84
|
+
this.logger.info(`HTTP Server is starting`);
|
|
85
85
|
|
|
86
86
|
this.server.on('request', this.handleRequest);
|
|
87
87
|
this.server.on('upgrade', this.handleWebSocket);
|
|
@@ -98,7 +98,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
98
98
|
},
|
|
99
99
|
undefined,
|
|
100
100
|
() => {
|
|
101
|
-
this.
|
|
101
|
+
this.logger.info(listenMessage);
|
|
102
102
|
r(undefined);
|
|
103
103
|
},
|
|
104
104
|
);
|
|
@@ -109,7 +109,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
109
109
|
request: http.IncomingMessage,
|
|
110
110
|
res: http.ServerResponse,
|
|
111
111
|
) => {
|
|
112
|
-
this.
|
|
112
|
+
this.logger.trace(
|
|
113
113
|
`Handling inbound HTTP request on "${request.method}: ${request.url}"`,
|
|
114
114
|
);
|
|
115
115
|
|
|
@@ -131,6 +131,11 @@ export class HTTPServer extends EventEmitter {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
if (req.method?.toLowerCase() === 'head') {
|
|
135
|
+
this.logger.debug(`Inbound HEAD request, setting to GET`);
|
|
136
|
+
req.method = 'GET';
|
|
137
|
+
}
|
|
138
|
+
|
|
134
139
|
if (
|
|
135
140
|
this.config.getAllowGetCalls() &&
|
|
136
141
|
req.method === 'GET' &&
|
|
@@ -145,17 +150,17 @@ export class HTTPServer extends EventEmitter {
|
|
|
145
150
|
const route = await this.router.getRouteForHTTPRequest(req);
|
|
146
151
|
|
|
147
152
|
if (!route) {
|
|
148
|
-
this.
|
|
153
|
+
this.logger.error(
|
|
149
154
|
`No matching HTTP route handler for "${req.method}: ${req.parsed.href}"`,
|
|
150
155
|
);
|
|
151
156
|
writeResponse(res, 404, 'Not Found');
|
|
152
157
|
return Promise.resolve();
|
|
153
158
|
}
|
|
154
159
|
|
|
155
|
-
this.
|
|
160
|
+
this.logger.trace(`Found matching HTTP route handler "${route.path}"`);
|
|
156
161
|
|
|
157
162
|
if (route?.auth) {
|
|
158
|
-
this.
|
|
163
|
+
this.logger.trace(`Authorizing HTTP request to "${request.url}"`);
|
|
159
164
|
const isPermitted = await this.token.isAuthorized(req, route);
|
|
160
165
|
|
|
161
166
|
if (!isPermitted) {
|
|
@@ -179,7 +184,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
if (route.querySchema) {
|
|
182
|
-
this.
|
|
187
|
+
this.logger.trace(`Validating route query-params with QUERY schema`);
|
|
183
188
|
try {
|
|
184
189
|
const schema = Enjoi.schema(route.querySchema);
|
|
185
190
|
const valid = schema.validate(req.queryParams, {
|
|
@@ -199,7 +204,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
199
204
|
)
|
|
200
205
|
.join('\n');
|
|
201
206
|
|
|
202
|
-
this.
|
|
207
|
+
this.logger.error(
|
|
203
208
|
`HTTP query-params contain errors sending 400:${errorDetails}`,
|
|
204
209
|
);
|
|
205
210
|
|
|
@@ -212,7 +217,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
212
217
|
return Promise.resolve();
|
|
213
218
|
}
|
|
214
219
|
} catch (e) {
|
|
215
|
-
this.
|
|
220
|
+
this.logger.error(`Error parsing body schema`, e);
|
|
216
221
|
writeResponse(
|
|
217
222
|
res,
|
|
218
223
|
500,
|
|
@@ -224,7 +229,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
224
229
|
}
|
|
225
230
|
|
|
226
231
|
if (route.bodySchema) {
|
|
227
|
-
this.
|
|
232
|
+
this.logger.trace(`Validating route payload with BODY schema`);
|
|
228
233
|
try {
|
|
229
234
|
const schema = Enjoi.schema(route.bodySchema);
|
|
230
235
|
const valid = schema.validate(body, { abortEarly: false });
|
|
@@ -242,7 +247,9 @@ export class HTTPServer extends EventEmitter {
|
|
|
242
247
|
)
|
|
243
248
|
.join('\n');
|
|
244
249
|
|
|
245
|
-
this.
|
|
250
|
+
this.logger.error(
|
|
251
|
+
`HTTP body contain errors sending 400:${errorDetails}`,
|
|
252
|
+
);
|
|
246
253
|
|
|
247
254
|
writeResponse(
|
|
248
255
|
res,
|
|
@@ -253,7 +260,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
253
260
|
return Promise.resolve();
|
|
254
261
|
}
|
|
255
262
|
} catch (e) {
|
|
256
|
-
this.
|
|
263
|
+
this.logger.error(`Error parsing body schema`, e);
|
|
257
264
|
writeResponse(
|
|
258
265
|
res,
|
|
259
266
|
500,
|
|
@@ -267,7 +274,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
267
274
|
return (route as HTTPRoute)
|
|
268
275
|
.handler(req, res, new this.Logger(route.name, req))
|
|
269
276
|
.then(() => {
|
|
270
|
-
this.
|
|
277
|
+
this.logger.trace('HTTP connection complete');
|
|
271
278
|
})
|
|
272
279
|
.catch((e) => {
|
|
273
280
|
if (e instanceof BadRequest) {
|
|
@@ -299,7 +306,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
299
306
|
socket: stream.Duplex,
|
|
300
307
|
head: Buffer,
|
|
301
308
|
) => {
|
|
302
|
-
this.
|
|
309
|
+
this.logger.trace(`Handling inbound WebSocket request on "${request.url}"`);
|
|
303
310
|
|
|
304
311
|
const req = request as Request;
|
|
305
312
|
const proceed = await this.hooks.before({ head, req, socket });
|
|
@@ -313,10 +320,14 @@ export class HTTPServer extends EventEmitter {
|
|
|
313
320
|
const route = await this.router.getRouteForWebSocketRequest(req);
|
|
314
321
|
|
|
315
322
|
if (route) {
|
|
316
|
-
this.
|
|
323
|
+
this.logger.trace(
|
|
324
|
+
`Found matching WebSocket route handler "${route.path}"`,
|
|
325
|
+
);
|
|
317
326
|
|
|
318
327
|
if (route?.auth) {
|
|
319
|
-
this.
|
|
328
|
+
this.logger.trace(
|
|
329
|
+
`Authorizing WebSocket request to "${req.parsed.href}"`,
|
|
330
|
+
);
|
|
320
331
|
const isPermitted = await this.token.isAuthorized(req, route);
|
|
321
332
|
|
|
322
333
|
if (!isPermitted) {
|
|
@@ -325,7 +336,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
325
336
|
}
|
|
326
337
|
|
|
327
338
|
if (route.querySchema) {
|
|
328
|
-
this.
|
|
339
|
+
this.logger.trace(`Validating route query-params with QUERY schema`);
|
|
329
340
|
try {
|
|
330
341
|
const schema = Enjoi.schema(route.querySchema);
|
|
331
342
|
const valid = schema.validate(req.queryParams, {
|
|
@@ -345,7 +356,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
345
356
|
)
|
|
346
357
|
.join('\n');
|
|
347
358
|
|
|
348
|
-
this.
|
|
359
|
+
this.logger.error(
|
|
349
360
|
`WebSocket query-params contain errors sending 400:${errorDetails}`,
|
|
350
361
|
);
|
|
351
362
|
|
|
@@ -358,7 +369,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
358
369
|
return Promise.resolve();
|
|
359
370
|
}
|
|
360
371
|
} catch (e) {
|
|
361
|
-
this.
|
|
372
|
+
this.logger.error(`Error parsing query-params schema`, e);
|
|
362
373
|
writeResponse(
|
|
363
374
|
socket,
|
|
364
375
|
500,
|
|
@@ -372,7 +383,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
372
383
|
return (route as WebSocketRoute)
|
|
373
384
|
.handler(req, socket, head, new this.Logger(route.name, req))
|
|
374
385
|
.then(() => {
|
|
375
|
-
this.
|
|
386
|
+
this.logger.trace('Websocket connection complete');
|
|
376
387
|
})
|
|
377
388
|
.catch((e) => {
|
|
378
389
|
if (e instanceof BadRequest) {
|
|
@@ -391,7 +402,7 @@ export class HTTPServer extends EventEmitter {
|
|
|
391
402
|
return writeResponse(socket, 429, e.message);
|
|
392
403
|
}
|
|
393
404
|
|
|
394
|
-
this.
|
|
405
|
+
this.logger.error(
|
|
395
406
|
`Error handling request at "${route.path}": ${e}\n${e.stack}`,
|
|
396
407
|
);
|
|
397
408
|
|
|
@@ -399,18 +410,20 @@ export class HTTPServer extends EventEmitter {
|
|
|
399
410
|
});
|
|
400
411
|
}
|
|
401
412
|
|
|
402
|
-
this.
|
|
413
|
+
this.logger.error(
|
|
414
|
+
`No matching WebSocket route handler for "${req.parsed.href}"`,
|
|
415
|
+
);
|
|
403
416
|
return writeResponse(socket, 404, 'Not Found');
|
|
404
417
|
};
|
|
405
418
|
|
|
406
419
|
public async shutdown(): Promise<void> {
|
|
407
|
-
this.
|
|
420
|
+
this.logger.info(`HTTP Server is shutting down`);
|
|
408
421
|
await new Promise((r) => this.server.close(r));
|
|
409
422
|
this.server && this.server.removeAllListeners();
|
|
410
423
|
|
|
411
424
|
// @ts-ignore garbage collect this reference
|
|
412
425
|
this.server = null;
|
|
413
|
-
this.
|
|
426
|
+
this.logger.info(`HTTP Server shutdown complete`);
|
|
414
427
|
}
|
|
415
428
|
|
|
416
429
|
/**
|
|
@@ -78,9 +78,10 @@ export default class ChromiumContentPostRoute extends BrowserHTTPRoute {
|
|
|
78
78
|
handler = async (
|
|
79
79
|
req: Request,
|
|
80
80
|
res: ServerResponse,
|
|
81
|
-
|
|
81
|
+
logger: Logger,
|
|
82
82
|
browser: BrowserInstance,
|
|
83
83
|
): Promise<void> => {
|
|
84
|
+
logger.info('Content API invoked with body:', req.body);
|
|
84
85
|
const contentType =
|
|
85
86
|
!req.headers.accept || req.headers.accept?.includes('*')
|
|
86
87
|
? contentTypes.html
|
|
@@ -166,6 +167,7 @@ export default class ChromiumContentPostRoute extends BrowserHTTPRoute {
|
|
|
166
167
|
!!rejectRequestPattern.find((pattern) => req.url().match(pattern)) ||
|
|
167
168
|
rejectResourceTypes.includes(req.resourceType())
|
|
168
169
|
) {
|
|
170
|
+
logger.debug(`Aborting request ${req.method()}: ${req.url()}`);
|
|
169
171
|
return req.abort();
|
|
170
172
|
}
|
|
171
173
|
const interceptor = requestInterceptors.find((r) =>
|
|
@@ -233,6 +235,8 @@ export default class ChromiumContentPostRoute extends BrowserHTTPRoute {
|
|
|
233
235
|
|
|
234
236
|
page.close().catch(noop);
|
|
235
237
|
|
|
238
|
+
logger.info('Content API request completed');
|
|
239
|
+
|
|
236
240
|
return writeResponse(res, 200, markup, contentTypes.html);
|
|
237
241
|
};
|
|
238
242
|
}
|