@browserless.io/browserless 2.0.0-beta-1 → 2.0.0-beta-2
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/bin/browserless.js +128 -8
- package/build/browserless.d.ts +23 -18
- package/build/browserless.js +24 -17
- package/build/browsers/cdp-chromium.d.ts +17 -14
- package/build/browsers/index.d.ts +27 -10
- package/build/browsers/index.js +2 -12
- package/build/browsers/playwright-chromium.d.ts +12 -9
- package/build/browsers/playwright-firefox.d.ts +12 -9
- package/build/browsers/playwright-webkit.d.ts +12 -9
- package/build/config.d.ts +31 -31
- package/build/exports.d.ts +2 -0
- package/build/exports.js +2 -0
- package/build/file-system.d.ts +2 -2
- package/build/limiter.d.ts +34 -11
- package/build/metrics.d.ts +17 -11
- package/build/monitoring.d.ts +3 -2
- package/build/router.d.ts +28 -0
- package/build/router.js +138 -0
- package/build/routes/chromium/http/content-post.body.json +8 -8
- package/build/routes/chromium/http/download-post.js +1 -1
- package/build/routes/chromium/http/function-post.js +1 -1
- package/build/routes/chromium/http/pdf-post.body.json +8 -8
- package/build/routes/chromium/http/performance.js +1 -1
- 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/chromium/utils/function/client.d.ts +3 -3
- package/build/routes/management/http/config-get.js +1 -1
- package/build/routes/management/http/metrics-get.js +1 -1
- package/build/routes/management/http/metrics-total-get.js +1 -1
- package/build/routes/management/http/sessions-get.js +3 -3
- package/build/routes/management/http/static-get.js +1 -1
- package/build/server.d.ts +22 -27
- package/build/server.js +29 -149
- package/build/token.d.ts +6 -0
- package/build/token.js +21 -0
- package/build/types.d.ts +85 -14
- package/build/utils.d.ts +1 -2
- package/build/utils.js +0 -13
- package/build/webhooks.d.ts +2 -2
- package/docker/sdk/Dockerfile +2 -6
- package/package.json +4 -4
- package/src/browserless.ts +44 -32
- package/src/browsers/cdp-chromium.ts +13 -13
- package/src/browsers/index.ts +9 -24
- package/src/browsers/playwright-chromium.ts +9 -9
- package/src/browsers/playwright-firefox.ts +9 -9
- package/src/browsers/playwright-webkit.ts +9 -9
- package/src/config.ts +32 -31
- package/src/exports.ts +2 -0
- package/src/file-system.ts +2 -2
- package/src/limiter.ts +11 -11
- package/src/metrics.ts +11 -11
- package/src/monitoring.ts +2 -2
- package/src/router.ts +234 -0
- package/src/routes/chromium/http/download-post.ts +1 -1
- package/src/routes/chromium/http/function-post.ts +1 -1
- package/src/routes/chromium/http/performance.ts +1 -1
- package/src/routes/chromium/utils/function/client.ts +2 -2
- package/src/routes/management/http/config-get.ts +1 -1
- package/src/routes/management/http/metrics-get.ts +1 -1
- package/src/routes/management/http/metrics-total-get.ts +1 -1
- package/src/routes/management/http/sessions-get.ts +3 -3
- package/src/routes/management/http/static-get.ts +1 -1
- package/src/server.ts +43 -238
- package/src/token.ts +40 -0
- package/src/types.ts +92 -17
- package/src/utils.ts +0 -25
- package/src/webhooks.ts +2 -2
- package/static/docs/swagger.json +9 -9
|
@@ -7,7 +7,7 @@ const route = {
|
|
|
7
7
|
contentTypes: [contentTypes.json],
|
|
8
8
|
description: `Gets total metric details from the time the server started.`,
|
|
9
9
|
handler: async (_req, res) => {
|
|
10
|
-
const { _fileSystem, _config } = route;
|
|
10
|
+
const { getFileSystem: _fileSystem, getConfig: _config } = route;
|
|
11
11
|
if (!_fileSystem || !_config) {
|
|
12
12
|
throw new ServerError(`Couldn't locate the file-system or config module`);
|
|
13
13
|
}
|
|
@@ -8,7 +8,7 @@ const route = {
|
|
|
8
8
|
contentTypes: [contentTypes.json],
|
|
9
9
|
description: `Gets total metric details summed from the time the server started.`,
|
|
10
10
|
handler: async (_req, res) => {
|
|
11
|
-
const { _fileSystem, _config } = route;
|
|
11
|
+
const { getFileSystem: _fileSystem, getConfig: _config } = route;
|
|
12
12
|
if (!_fileSystem || !_config) {
|
|
13
13
|
throw new ServerError(`Couldn't locate the file-system or config module`);
|
|
14
14
|
}
|
|
@@ -6,12 +6,12 @@ const route = {
|
|
|
6
6
|
concurrency: false,
|
|
7
7
|
contentTypes: [contentTypes.json],
|
|
8
8
|
description: `Lists all currently running sessions and relevant meta-data excluding potentially open pages.`,
|
|
9
|
-
handler: async (
|
|
10
|
-
const {
|
|
9
|
+
handler: async (_req, res) => {
|
|
10
|
+
const { getBrowserManager: browserManager } = route;
|
|
11
11
|
if (!browserManager) {
|
|
12
12
|
throw new BadRequest(`Couldn't load browsers running`);
|
|
13
13
|
}
|
|
14
|
-
const response = await browserManager().getAllSessions(
|
|
14
|
+
const response = await browserManager().getAllSessions();
|
|
15
15
|
return jsonResponse(res, 200, response);
|
|
16
16
|
},
|
|
17
17
|
method: Methods.get,
|
|
@@ -28,7 +28,7 @@ const route = {
|
|
|
28
28
|
contentTypes: [contentTypes.any],
|
|
29
29
|
description: `Serves static files inside of this "static" directory. Content-types will vary depending on the type of file being returned.`,
|
|
30
30
|
handler: async (req, res) => {
|
|
31
|
-
const {
|
|
31
|
+
const { getConfig: getConfig } = route;
|
|
32
32
|
const { pathname } = req.parsed;
|
|
33
33
|
const fileCache = pathMap.get(pathname);
|
|
34
34
|
if (fileCache) {
|
package/build/server.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="debug" />
|
|
3
|
+
/// <reference types="node" />
|
|
4
|
+
/// <reference types="node" />
|
|
5
|
+
import * as http from 'http';
|
|
6
|
+
import * as stream from 'stream';
|
|
7
|
+
import { Config, Metrics, Request, Response, Router, Token } from '@browserless.io/browserless';
|
|
2
8
|
export interface HTTPServerOptions {
|
|
3
9
|
concurrent: number;
|
|
4
10
|
host: string;
|
|
@@ -7,32 +13,21 @@ export interface HTTPServerOptions {
|
|
|
7
13
|
timeout: number;
|
|
8
14
|
}
|
|
9
15
|
export declare class HTTPServer {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
private onQueueFullHTTP;
|
|
23
|
-
private onQueueFullWebSocket;
|
|
24
|
-
private onHTTPTimeout;
|
|
25
|
-
private onWebsocketTimeout;
|
|
26
|
-
private onHTTPUnauthorized;
|
|
27
|
-
private onWebsocketUnauthorized;
|
|
28
|
-
private wrapHTTPHandler;
|
|
29
|
-
private wrapWebSocketHandler;
|
|
30
|
-
private getTimeout;
|
|
31
|
-
private registerHTTPRoute;
|
|
32
|
-
private registerWebSocketRoute;
|
|
16
|
+
protected config: Config;
|
|
17
|
+
protected metrics: Metrics;
|
|
18
|
+
protected token: Token;
|
|
19
|
+
protected router: Router;
|
|
20
|
+
protected server: http.Server;
|
|
21
|
+
protected port: number;
|
|
22
|
+
protected host?: string;
|
|
23
|
+
protected log: import("debug").Debugger;
|
|
24
|
+
protected verbose: import("debug").Debugger;
|
|
25
|
+
constructor(config: Config, metrics: Metrics, token: Token, router: Router);
|
|
26
|
+
protected onHTTPUnauthorized: (_req: Request, res: Response) => void;
|
|
27
|
+
protected onWebsocketUnauthorized: (_req: Request, socket: stream.Duplex) => void;
|
|
33
28
|
start(): Promise<void>;
|
|
34
29
|
stop(): Promise<void>;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
protected tearDown(): void;
|
|
31
|
+
protected handleRequest: (request: http.IncomingMessage, res: http.ServerResponse) => Promise<void | http.ServerResponse<http.IncomingMessage>>;
|
|
32
|
+
protected handleWebSocket: (request: http.IncomingMessage, socket: stream.Duplex, head: Buffer) => Promise<void>;
|
|
38
33
|
}
|
package/build/server.js
CHANGED
|
@@ -1,49 +1,26 @@
|
|
|
1
1
|
import * as http from 'http';
|
|
2
|
-
import { BadRequest,
|
|
2
|
+
import { BadRequest, NotFound, Timeout, TooManyRequests, Unauthorized, beforeRequest, contentTypes, convertPathToURL, createLogger, queryParamsToObject, readBody, shimLegacyRequests, writeResponse, } from '@browserless.io/browserless';
|
|
3
3
|
// @ts-ignore
|
|
4
4
|
import Enjoi from 'enjoi';
|
|
5
|
-
import micromatch from 'micromatch';
|
|
6
5
|
export class HTTPServer {
|
|
7
6
|
config;
|
|
8
7
|
metrics;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
httpRoutes;
|
|
12
|
-
webSocketRoutes;
|
|
8
|
+
token;
|
|
9
|
+
router;
|
|
13
10
|
server = http.createServer();
|
|
14
11
|
port;
|
|
15
12
|
host;
|
|
16
13
|
log = createLogger('server');
|
|
17
14
|
verbose = createLogger('server:verbose');
|
|
18
|
-
constructor(config, metrics,
|
|
15
|
+
constructor(config, metrics, token, router) {
|
|
19
16
|
this.config = config;
|
|
20
17
|
this.metrics = metrics;
|
|
21
|
-
this.
|
|
22
|
-
this.
|
|
23
|
-
this.httpRoutes = httpRoutes;
|
|
24
|
-
this.webSocketRoutes = webSocketRoutes;
|
|
18
|
+
this.token = token;
|
|
19
|
+
this.router = router;
|
|
25
20
|
this.host = config.getHost();
|
|
26
21
|
this.port = config.getPort();
|
|
27
|
-
this.httpRoutes = httpRoutes.map((r) => this.registerHTTPRoute(r));
|
|
28
|
-
this.webSocketRoutes = webSocketRoutes.map((r) => this.registerWebSocketRoute(r));
|
|
29
22
|
this.log(`Server instantiated with host "${this.host}" on port "${this.port}" using token "${this.config.getToken()}"`);
|
|
30
23
|
}
|
|
31
|
-
onQueueFullHTTP = (_req, res) => {
|
|
32
|
-
this.log(`Queue is full, sending 429 response`);
|
|
33
|
-
return writeResponse(res, 429, 'Too many requests');
|
|
34
|
-
};
|
|
35
|
-
onQueueFullWebSocket = (_req, socket) => {
|
|
36
|
-
this.log(`Queue is full, sending 429 response`);
|
|
37
|
-
return writeResponse(socket, 429, 'Too many requests');
|
|
38
|
-
};
|
|
39
|
-
onHTTPTimeout = (_req, res) => {
|
|
40
|
-
this.log(`HTTP job has timedout, sending 429 response`);
|
|
41
|
-
return writeResponse(res, 408, 'Request has timed out');
|
|
42
|
-
};
|
|
43
|
-
onWebsocketTimeout = (_req, socket) => {
|
|
44
|
-
this.log(`Websocket job has timedout, sending 429 response`);
|
|
45
|
-
return writeResponse(socket, 408, 'Request has timed out');
|
|
46
|
-
};
|
|
47
24
|
onHTTPUnauthorized = (_req, res) => {
|
|
48
25
|
this.log(`HTTP request is not properly authorized, responding with 401`);
|
|
49
26
|
this.metrics.addUnauthorized();
|
|
@@ -54,87 +31,6 @@ export class HTTPServer {
|
|
|
54
31
|
this.metrics.addUnauthorized();
|
|
55
32
|
return writeResponse(socket, 401, 'Bad or missing authentication.');
|
|
56
33
|
};
|
|
57
|
-
wrapHTTPHandler = (route, handler) => async (req, res) => {
|
|
58
|
-
if (!isConnected(res)) {
|
|
59
|
-
this.log(`HTTP Request has closed prior to running`);
|
|
60
|
-
return Promise.resolve();
|
|
61
|
-
}
|
|
62
|
-
if (route.browser) {
|
|
63
|
-
const browser = await this.browserManager.getBrowserForRequest(req, route);
|
|
64
|
-
if (!isConnected(res)) {
|
|
65
|
-
this.log(`HTTP Request has closed prior to running`);
|
|
66
|
-
this.browserManager.complete(browser);
|
|
67
|
-
return Promise.resolve();
|
|
68
|
-
}
|
|
69
|
-
if (!browser) {
|
|
70
|
-
return writeResponse(res, 500, `Error loading the browser.`);
|
|
71
|
-
}
|
|
72
|
-
if (!isConnected(res)) {
|
|
73
|
-
this.log(`HTTP Request has closed prior to running`);
|
|
74
|
-
return Promise.resolve();
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
this.verbose(`Running found HTTP handler.`);
|
|
78
|
-
return await handler(req, res, browser);
|
|
79
|
-
}
|
|
80
|
-
finally {
|
|
81
|
-
this.verbose(`HTTP Request handler has finished.`);
|
|
82
|
-
this.browserManager.complete(browser);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return handler(req, res);
|
|
86
|
-
};
|
|
87
|
-
wrapWebSocketHandler = (route, handler) => async (req, socket, head) => {
|
|
88
|
-
if (!isConnected(socket)) {
|
|
89
|
-
this.log(`WebSocket Request has closed prior to running`);
|
|
90
|
-
return Promise.resolve();
|
|
91
|
-
}
|
|
92
|
-
if (route.browser) {
|
|
93
|
-
const browser = await this.browserManager.getBrowserForRequest(req, route);
|
|
94
|
-
if (!isConnected(socket)) {
|
|
95
|
-
this.log(`WebSocket Request has closed prior to running`);
|
|
96
|
-
this.browserManager.complete(browser);
|
|
97
|
-
return Promise.resolve();
|
|
98
|
-
}
|
|
99
|
-
if (!browser) {
|
|
100
|
-
return writeResponse(socket, 500, `Error loading the browser.`);
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
this.verbose(`Running found WebSocket handler.`);
|
|
104
|
-
await handler(req, socket, head, browser);
|
|
105
|
-
}
|
|
106
|
-
finally {
|
|
107
|
-
this.verbose(`WebSocket Request handler has finished.`);
|
|
108
|
-
this.browserManager.complete(browser);
|
|
109
|
-
}
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
return handler(req, socket, head);
|
|
113
|
-
};
|
|
114
|
-
getTimeout(req) {
|
|
115
|
-
const timer = req.parsed.searchParams.get('timeout');
|
|
116
|
-
return timer ? +timer : undefined;
|
|
117
|
-
}
|
|
118
|
-
registerHTTPRoute(route) {
|
|
119
|
-
this.verbose(`Registering HTTP ${route.method.toUpperCase()} ${route.path}`);
|
|
120
|
-
route._browserManager = () => this.browserManager;
|
|
121
|
-
const bound = route.handler.bind(route);
|
|
122
|
-
const wrapped = this.wrapHTTPHandler(route, bound);
|
|
123
|
-
route.handler = route.concurrency
|
|
124
|
-
? this.limiter.limit(wrapped, this.onQueueFullHTTP, this.onHTTPTimeout, this.getTimeout)
|
|
125
|
-
: wrapped;
|
|
126
|
-
return route;
|
|
127
|
-
}
|
|
128
|
-
registerWebSocketRoute(route) {
|
|
129
|
-
this.verbose(`Registering WebSocket "${route.path}"`);
|
|
130
|
-
route._browserManager = () => this.browserManager;
|
|
131
|
-
const bound = route.handler.bind(route);
|
|
132
|
-
const wrapped = this.wrapWebSocketHandler(route, bound);
|
|
133
|
-
route.handler = route.concurrency
|
|
134
|
-
? this.limiter.limit(wrapped, this.onQueueFullWebSocket, this.onWebsocketTimeout, this.getTimeout)
|
|
135
|
-
: wrapped;
|
|
136
|
-
return route;
|
|
137
|
-
}
|
|
138
34
|
async start() {
|
|
139
35
|
this.log(`HTTP Server is starting`);
|
|
140
36
|
this.server.on('request', this.handleRequest);
|
|
@@ -156,14 +52,12 @@ export class HTTPServer {
|
|
|
156
52
|
async stop() {
|
|
157
53
|
this.log(`HTTP Server is shutting down`);
|
|
158
54
|
await new Promise((r) => this.server.close(r));
|
|
159
|
-
await Promise.all([this.tearDown(), this.
|
|
55
|
+
await Promise.all([this.tearDown(), this.router.teardown()]);
|
|
160
56
|
this.log(`HTTP Server shutdown complete`);
|
|
161
57
|
}
|
|
162
58
|
tearDown() {
|
|
163
59
|
this.log(`Tearing down all listeners and internal routes`);
|
|
164
60
|
this.server && this.server.removeAllListeners();
|
|
165
|
-
this.httpRoutes = [];
|
|
166
|
-
this.webSocketRoutes = [];
|
|
167
61
|
// @ts-ignore garbage collect this reference
|
|
168
62
|
this.server = null;
|
|
169
63
|
}
|
|
@@ -175,7 +69,6 @@ export class HTTPServer {
|
|
|
175
69
|
shimLegacyRequests(req.parsed);
|
|
176
70
|
if (!proceed)
|
|
177
71
|
return;
|
|
178
|
-
const staticHandler = this.httpRoutes.find((route) => route.path === HTTPManagementRoutes.static);
|
|
179
72
|
if (this.config.getAllowCORS()) {
|
|
180
73
|
Object.entries(this.config.getCORSHeaders()).forEach(([header, value]) => res.setHeader(header, value));
|
|
181
74
|
if (req.method === 'OPTIONS') {
|
|
@@ -191,24 +84,16 @@ export class HTTPServer {
|
|
|
191
84
|
req.body = req.parsed.searchParams.get('body');
|
|
192
85
|
req.parsed.searchParams.delete('body');
|
|
193
86
|
}
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
const found = this.httpRoutes.find((r) => micromatch.isMatch(req.parsed.pathname, r.path) &&
|
|
197
|
-
r.method === req.method?.toLocaleLowerCase() &&
|
|
198
|
-
(accepts.some((a) => a.startsWith('*/*')) ||
|
|
199
|
-
r.contentTypes.some((contentType) => accepts.includes(contentType))) &&
|
|
200
|
-
((!contentType && r.accepts.includes(contentTypes.any)) ||
|
|
201
|
-
r.accepts.includes(contentType))) || (req.method?.toLowerCase() === 'get' ? staticHandler : null);
|
|
202
|
-
if (!found) {
|
|
87
|
+
const route = await this.router.getRouteForHTTPRequest(req);
|
|
88
|
+
if (!route) {
|
|
203
89
|
this.log(`No matching WebSocket route handler for "${req.parsed.href}"`);
|
|
204
90
|
writeResponse(res, 404, 'Not Found');
|
|
205
91
|
return Promise.resolve();
|
|
206
92
|
}
|
|
207
|
-
this.verbose(`Found matching HTTP route handler "${
|
|
208
|
-
if (
|
|
93
|
+
this.verbose(`Found matching HTTP route handler "${route.path}"`);
|
|
94
|
+
if (route?.auth) {
|
|
209
95
|
this.verbose(`Authorizing HTTP request to "${request.url}"`);
|
|
210
|
-
const
|
|
211
|
-
const isPermitted = isAuthorized(req, found, tokens);
|
|
96
|
+
const isPermitted = await this.token.isAuthorized(req, route);
|
|
212
97
|
if (!isPermitted) {
|
|
213
98
|
return this.onHTTPUnauthorized(req, res);
|
|
214
99
|
}
|
|
@@ -217,17 +102,17 @@ export class HTTPServer {
|
|
|
217
102
|
req.body = body;
|
|
218
103
|
req.queryParams = queryParamsToObject(req.parsed.searchParams);
|
|
219
104
|
if (((req.headers['content-type']?.includes(contentTypes.json) ||
|
|
220
|
-
(
|
|
221
|
-
|
|
105
|
+
(route.accepts.length === 1 &&
|
|
106
|
+
route.accepts.includes(contentTypes.json))) &&
|
|
222
107
|
typeof body !== 'object') ||
|
|
223
108
|
body === null) {
|
|
224
109
|
writeResponse(res, 400, `Couldn't parse JSON body`);
|
|
225
110
|
return Promise.resolve();
|
|
226
111
|
}
|
|
227
|
-
if (
|
|
112
|
+
if (route.querySchema) {
|
|
228
113
|
this.verbose(`Validating route query-params with QUERY schema`);
|
|
229
114
|
try {
|
|
230
|
-
const schema = Enjoi.schema(
|
|
115
|
+
const schema = Enjoi.schema(route.querySchema);
|
|
231
116
|
const valid = schema.validate(req.queryParams, {
|
|
232
117
|
abortEarly: false,
|
|
233
118
|
});
|
|
@@ -246,10 +131,10 @@ export class HTTPServer {
|
|
|
246
131
|
return Promise.resolve();
|
|
247
132
|
}
|
|
248
133
|
}
|
|
249
|
-
if (
|
|
134
|
+
if (route.bodySchema) {
|
|
250
135
|
this.verbose(`Validating route payload with BODY schema`);
|
|
251
136
|
try {
|
|
252
|
-
const schema = Enjoi.schema(
|
|
137
|
+
const schema = Enjoi.schema(route.bodySchema);
|
|
253
138
|
const valid = schema.validate(body, { abortEarly: false });
|
|
254
139
|
if (valid.error) {
|
|
255
140
|
const errorDetails = valid.error.details
|
|
@@ -266,9 +151,7 @@ export class HTTPServer {
|
|
|
266
151
|
return Promise.resolve();
|
|
267
152
|
}
|
|
268
153
|
}
|
|
269
|
-
|
|
270
|
-
// argument for this to to work properly
|
|
271
|
-
return found
|
|
154
|
+
return route
|
|
272
155
|
.handler(req, res)
|
|
273
156
|
.then(() => {
|
|
274
157
|
this.verbose('HTTP connection complete');
|
|
@@ -289,7 +172,7 @@ export class HTTPServer {
|
|
|
289
172
|
if (e instanceof Timeout) {
|
|
290
173
|
return writeResponse(res, 408, e.message);
|
|
291
174
|
}
|
|
292
|
-
this.log(`Error handling request at "${
|
|
175
|
+
this.log(`Error handling request at "${route.path}": ${e}`);
|
|
293
176
|
return writeResponse(res, 500, e.toString());
|
|
294
177
|
});
|
|
295
178
|
};
|
|
@@ -301,22 +184,21 @@ export class HTTPServer {
|
|
|
301
184
|
shimLegacyRequests(req.parsed);
|
|
302
185
|
if (!proceed)
|
|
303
186
|
return;
|
|
304
|
-
const { pathname } = req.parsed;
|
|
305
187
|
req.queryParams = queryParamsToObject(req.parsed.searchParams);
|
|
306
|
-
const
|
|
307
|
-
if (
|
|
308
|
-
this.verbose(`Found matching WebSocket route handler "${
|
|
309
|
-
if (
|
|
188
|
+
const route = await this.router.getRouteForWebSocketRequest(req);
|
|
189
|
+
if (route) {
|
|
190
|
+
this.verbose(`Found matching WebSocket route handler "${route.path}"`);
|
|
191
|
+
if (route?.auth) {
|
|
310
192
|
this.verbose(`Authorizing WebSocket request to "${req.parsed.href}"`);
|
|
311
|
-
const isPermitted = isAuthorized(req,
|
|
193
|
+
const isPermitted = await this.token.isAuthorized(req, route);
|
|
312
194
|
if (!isPermitted) {
|
|
313
195
|
return this.onWebsocketUnauthorized(req, socket);
|
|
314
196
|
}
|
|
315
197
|
}
|
|
316
|
-
if (
|
|
198
|
+
if (route.querySchema) {
|
|
317
199
|
this.verbose(`Validating route query-params with QUERY schema`);
|
|
318
200
|
try {
|
|
319
|
-
const schema = Enjoi.schema(
|
|
201
|
+
const schema = Enjoi.schema(route.querySchema);
|
|
320
202
|
const valid = schema.validate(req.queryParams, {
|
|
321
203
|
abortEarly: false,
|
|
322
204
|
});
|
|
@@ -335,9 +217,7 @@ export class HTTPServer {
|
|
|
335
217
|
return Promise.resolve();
|
|
336
218
|
}
|
|
337
219
|
}
|
|
338
|
-
|
|
339
|
-
// argument for this to to work properly
|
|
340
|
-
return found
|
|
220
|
+
return route
|
|
341
221
|
.handler(req, socket, head)
|
|
342
222
|
.then(() => {
|
|
343
223
|
this.verbose('Websocket connection complete');
|
|
@@ -355,7 +235,7 @@ export class HTTPServer {
|
|
|
355
235
|
if (e instanceof TooManyRequests) {
|
|
356
236
|
return writeResponse(socket, 429, e.message);
|
|
357
237
|
}
|
|
358
|
-
this.log(`Error handling request at "${
|
|
238
|
+
this.log(`Error handling request at "${route.path}": ${e}\n${e.stack}`);
|
|
359
239
|
return writeResponse(socket, 500, e.message);
|
|
360
240
|
});
|
|
361
241
|
}
|
package/build/token.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { BrowserHTTPRoute, BrowserWebsocketRoute, Config, HTTPRoute, Request, WebSocketRoute } from '@browserless.io/browserless';
|
|
2
|
+
export declare class Token {
|
|
3
|
+
protected config: Config;
|
|
4
|
+
constructor(config: Config);
|
|
5
|
+
isAuthorized: (req: Request, route: BrowserHTTPRoute | BrowserWebsocketRoute | HTTPRoute | WebSocketRoute) => Promise<boolean>;
|
|
6
|
+
}
|
package/build/token.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getTokenFromRequest, } from '@browserless.io/browserless';
|
|
2
|
+
export class Token {
|
|
3
|
+
config;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
isAuthorized = async (req, route) => {
|
|
8
|
+
const token = this.config.getToken();
|
|
9
|
+
if (token === null) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (route.auth !== true) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
const requestToken = getTokenFromRequest(req);
|
|
16
|
+
if (!requestToken) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return (Array.isArray(token) ? token : [token]).includes(requestToken);
|
|
20
|
+
};
|
|
21
|
+
}
|
package/build/types.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/// <reference types="debug" />
|
|
5
5
|
import * as http from 'http';
|
|
6
6
|
import * as stream from 'stream';
|
|
7
|
-
import { APITags,
|
|
7
|
+
import { APITags, Browserless, CDPChromium, Config, HTTPManagementRoutes, HTTPRoutes, Methods, Metrics, PlaywrightChromium, PlaywrightFirefox, PlaywrightWebkit, Request, WebsocketRoutes, contentTypes } from '@browserless.io/browserless';
|
|
8
8
|
import { HTTPRequest, Page, ResponseForRequest, ScreenshotOptions } from 'puppeteer-core';
|
|
9
9
|
export interface BeforeRequest {
|
|
10
10
|
head?: Buffer;
|
|
@@ -42,43 +42,87 @@ export interface BrowserJSON {
|
|
|
42
42
|
'V8-Version': string;
|
|
43
43
|
'WebKit-Version': string;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The default launch options or a function, accepting
|
|
47
|
+
* the request object, that produces the launch options.
|
|
48
|
+
*/
|
|
45
49
|
type defaultLaunchOptions = CDPLaunchOptions | BrowserlessLaunch | ((req: Request) => CDPLaunchOptions | BrowserlessLaunch);
|
|
46
50
|
interface Route {
|
|
47
|
-
_browserManager?: () => BrowserManager;
|
|
48
|
-
_config?: () => Config;
|
|
49
|
-
_debug?: () => debug.Debugger;
|
|
50
|
-
_fileSystem?: () => FileSystem;
|
|
51
|
-
_metrics?: () => Metrics;
|
|
52
|
-
_monitor?: () => Monitoring;
|
|
53
51
|
/**
|
|
54
|
-
*
|
|
52
|
+
* A boolean, or a function that returns a boolean, on
|
|
53
|
+
* whether the route requires an API token to access.
|
|
55
54
|
*/
|
|
56
55
|
auth: boolean | ((req: Request) => Promise<boolean>);
|
|
57
56
|
/**
|
|
58
57
|
* The schematic of the submitted BODY (typically)
|
|
59
|
-
* an object when the route is json-based.
|
|
58
|
+
* an object when the route is json-based. This is generated
|
|
59
|
+
* automatically if your route defines a BodySchema type.
|
|
60
60
|
*/
|
|
61
61
|
bodySchema?: unknown;
|
|
62
62
|
/**
|
|
63
63
|
* Whether the route should be bound by the global
|
|
64
|
-
* concurrency limit
|
|
64
|
+
* concurrency limit defined in your configuration.
|
|
65
65
|
*/
|
|
66
66
|
concurrency: boolean;
|
|
67
67
|
/**
|
|
68
|
-
* Description of the route and what it does
|
|
68
|
+
* Description of the route and what it does. This description
|
|
69
|
+
* is then used in the embedded documentation site.
|
|
69
70
|
*/
|
|
70
71
|
description: string;
|
|
71
72
|
/**
|
|
72
|
-
*
|
|
73
|
+
* Helper function to load the browser-manager instance. Defined
|
|
74
|
+
* and injected by browserless after initialization.
|
|
75
|
+
* @returns BrowserManager
|
|
76
|
+
*/
|
|
77
|
+
getBrowserManager?: () => Browserless['browserManager'];
|
|
78
|
+
/**
|
|
79
|
+
* Helper function that loads the config module. Defined and injected by
|
|
80
|
+
* browserless after initialization.
|
|
81
|
+
* @returns Config
|
|
82
|
+
*/
|
|
83
|
+
getConfig?: () => Browserless['config'];
|
|
84
|
+
/**
|
|
85
|
+
* Helper function that loads the debug module, useful
|
|
86
|
+
* for logging messages scoped to the routes path. Defined
|
|
87
|
+
* and injected by browserless after initialization.
|
|
88
|
+
* @returns Debug
|
|
89
|
+
*/
|
|
90
|
+
getDebug?: () => debug.Debugger;
|
|
91
|
+
/**
|
|
92
|
+
* Helper function that loads the file-system module
|
|
93
|
+
* for interacting with file-systems. Defined and injected by
|
|
94
|
+
* browserless after initialization.
|
|
95
|
+
* @returns FileSystem
|
|
96
|
+
*/
|
|
97
|
+
getFileSystem?: () => Browserless['fileSystem'];
|
|
98
|
+
/**
|
|
99
|
+
* Helper function that loads the metrics module for
|
|
100
|
+
* collecting and aggregating statistics. Defined and injected by
|
|
101
|
+
* browserless after initialization.
|
|
102
|
+
* @returns Metrics
|
|
103
|
+
*/
|
|
104
|
+
getMetrics?: () => Browserless['metrics'];
|
|
105
|
+
/**
|
|
106
|
+
* Helper function that loads the monitoring module useful
|
|
107
|
+
* for monitoring system health. Defined and injected by
|
|
108
|
+
* browserless after initialization.
|
|
109
|
+
* @returns Monitor
|
|
110
|
+
*/
|
|
111
|
+
getMonitoring?: () => Browserless['monitoring'];
|
|
112
|
+
/**
|
|
113
|
+
* The HTTP path that this route handles, eg '/my-route'
|
|
73
114
|
*/
|
|
74
115
|
path: HTTPRoutes | WebsocketRoutes | HTTPManagementRoutes | string;
|
|
75
116
|
/**
|
|
76
117
|
* The query parameters accepted by the route, defined in
|
|
77
|
-
* an object format.
|
|
118
|
+
* an object format. This is auto-generated for you if your
|
|
119
|
+
* route defines and exports a QuerySchema type.
|
|
78
120
|
*/
|
|
79
121
|
querySchema?: unknown;
|
|
80
122
|
/**
|
|
81
|
-
* The structure of the routes response when successful
|
|
123
|
+
* The structure of the routes response when successful. This
|
|
124
|
+
* is auto-generated for you if your route defines a ResponseSchema
|
|
125
|
+
* type and exports it in your route.
|
|
82
126
|
*/
|
|
83
127
|
responseSchema?: unknown;
|
|
84
128
|
/**
|
|
@@ -87,6 +131,11 @@ interface Route {
|
|
|
87
131
|
*/
|
|
88
132
|
tags: APITags[];
|
|
89
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* A primitive HTTP-based route that doesn't require a
|
|
136
|
+
* browser in order to fulfill requests. Used by downstream HTTPRoute
|
|
137
|
+
* and WebSocketRoute
|
|
138
|
+
*/
|
|
90
139
|
interface BasicHTTPRoute extends Route {
|
|
91
140
|
/**
|
|
92
141
|
* The allowed Content-Types that this route can read and handle.
|
|
@@ -105,6 +154,10 @@ interface BasicHTTPRoute extends Route {
|
|
|
105
154
|
*/
|
|
106
155
|
method: Methods;
|
|
107
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* A HTTP-based route, with a handler, that can fulfill requests without
|
|
159
|
+
* a browser required.
|
|
160
|
+
*/
|
|
108
161
|
export interface HTTPRoute extends BasicHTTPRoute {
|
|
109
162
|
browser: null;
|
|
110
163
|
/**
|
|
@@ -112,6 +165,11 @@ export interface HTTPRoute extends BasicHTTPRoute {
|
|
|
112
165
|
*/
|
|
113
166
|
handler: (req: Request, res: http.ServerResponse) => Promise<unknown>;
|
|
114
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* A HTTP-based route, with a handler, that can fulfill requests but
|
|
170
|
+
* requires a browser in order to do so. Handler will then be called
|
|
171
|
+
* with a 3rd argument of the browser class specified.
|
|
172
|
+
*/
|
|
115
173
|
export interface BrowserHTTPRoute extends BasicHTTPRoute {
|
|
116
174
|
browser: BrowserClasses;
|
|
117
175
|
defaultLaunchOptions?: defaultLaunchOptions;
|
|
@@ -122,6 +180,10 @@ export interface BrowserHTTPRoute extends BasicHTTPRoute {
|
|
|
122
180
|
handler: (req: Request, res: http.ServerResponse, browser: BrowserInstance) => Promise<unknown>;
|
|
123
181
|
onNewPage?: undefined;
|
|
124
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* A WebSocket-based route, with a handler, that can fulfill requests
|
|
185
|
+
* that do not require a browser in order to operate.
|
|
186
|
+
*/
|
|
125
187
|
export interface WebSocketRoute extends Route {
|
|
126
188
|
browser: null;
|
|
127
189
|
/**
|
|
@@ -129,6 +191,11 @@ export interface WebSocketRoute extends Route {
|
|
|
129
191
|
*/
|
|
130
192
|
handler: (req: Request, socket: stream.Duplex, head: Buffer) => Promise<unknown>;
|
|
131
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* A WebSocket-based route, with a handler, that can fulfill requests
|
|
196
|
+
* that need a browser. Handler is called with an additional argument of
|
|
197
|
+
* browser (the browser class required to run the route).
|
|
198
|
+
*/
|
|
132
199
|
export interface BrowserWebsocketRoute extends Route {
|
|
133
200
|
browser: BrowserClasses;
|
|
134
201
|
defaultLaunchOptions?: defaultLaunchOptions;
|
|
@@ -137,6 +204,10 @@ export interface BrowserWebsocketRoute extends Route {
|
|
|
137
204
|
* with the prior set browser being injected.
|
|
138
205
|
*/
|
|
139
206
|
handler(req: Request, socket: stream.Duplex, head: Buffer, browser: BrowserInstance): Promise<unknown>;
|
|
207
|
+
/**
|
|
208
|
+
* An optional function to automatically set up or handle new page
|
|
209
|
+
* creation. Useful for injecting behaviors or other functionality.
|
|
210
|
+
*/
|
|
140
211
|
onNewPage?: (url: URL, page: Page) => Promise<void>;
|
|
141
212
|
}
|
|
142
213
|
interface BrowserlessLaunch {
|
package/build/utils.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
/// <reference types="node" />
|
|
4
|
-
import {
|
|
4
|
+
import { CDPChromium, Config, PlaywrightChromium, PlaywrightFirefox, PlaywrightWebkit, Request, WaitForEventOptions, WaitForFunctionOptions, codes, contentTypes } from '@browserless.io/browserless';
|
|
5
5
|
import { CDPSession } from 'playwright-core';
|
|
6
6
|
import { Duplex } from 'stream';
|
|
7
7
|
import { Page } from 'puppeteer-core';
|
|
@@ -19,7 +19,6 @@ export declare const writeResponse: (writeable: Duplex | ServerResponse, httpCod
|
|
|
19
19
|
export declare const jsonResponse: (response: ServerResponse, httpCode?: keyof typeof codes, json?: unknown, allowNull?: boolean) => void;
|
|
20
20
|
export declare const fetchJson: (url: string, init?: RequestInit | undefined) => Promise<unknown>;
|
|
21
21
|
export declare const getTokenFromRequest: (req: Request) => string | null;
|
|
22
|
-
export declare const isAuthorized: (req: Request, route: BrowserHTTPRoute | BrowserWebsocketRoute | HTTPRoute | WebSocketRoute, token: string | string[] | null) => boolean;
|
|
23
22
|
export declare const readRequestBody: (req: Request) => Promise<string>;
|
|
24
23
|
export declare const safeParse: (maybeJson: string) => unknown | null;
|
|
25
24
|
export declare const removeNullStringify: (json: unknown, allowNull?: boolean) => string;
|
package/build/utils.js
CHANGED
|
@@ -118,19 +118,6 @@ export const getTokenFromRequest = (req) => {
|
|
|
118
118
|
const tokenParam = req.parsed.searchParams.get('token');
|
|
119
119
|
return tokenParam ?? getAuthHeaderToken(authHeader || '');
|
|
120
120
|
};
|
|
121
|
-
export const isAuthorized = (req, route, token) => {
|
|
122
|
-
if (token === null) {
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
if (route.auth === false) {
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
const requestToken = getTokenFromRequest(req);
|
|
129
|
-
if (!requestToken) {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
return (Array.isArray(token) ? token : [token]).includes(requestToken);
|
|
133
|
-
};
|
|
134
121
|
// NOTE, if proxying request elsewhere, you must re-stream the body again
|
|
135
122
|
export const readRequestBody = async (req) => {
|
|
136
123
|
return new Promise((resolve) => {
|
package/build/webhooks.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Config } from '@browserless.io/browserless';
|
|
2
2
|
export declare class WebHooks {
|
|
3
|
-
|
|
3
|
+
protected config: Config;
|
|
4
4
|
constructor(config: Config);
|
|
5
|
-
|
|
5
|
+
protected callURL(url: string | null): Promise<void | Response> | undefined;
|
|
6
6
|
callFailedHealthURL(): Promise<void | Response> | undefined;
|
|
7
7
|
callQueueAlertURL(): Promise<void | Response> | undefined;
|
|
8
8
|
callRejectAlertURL(): Promise<void | Response> | undefined;
|