@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
package/src/server.ts
CHANGED
|
@@ -2,19 +2,15 @@ import * as http from 'http';
|
|
|
2
2
|
import * as stream from 'stream';
|
|
3
3
|
import {
|
|
4
4
|
BadRequest,
|
|
5
|
-
BrowserHTTPRoute,
|
|
6
|
-
BrowserManager,
|
|
7
|
-
BrowserWebsocketRoute,
|
|
8
5
|
Config,
|
|
9
|
-
HTTPManagementRoutes,
|
|
10
6
|
HTTPRoute,
|
|
11
|
-
Limiter,
|
|
12
|
-
Methods,
|
|
13
7
|
Metrics,
|
|
14
8
|
NotFound,
|
|
15
9
|
Request,
|
|
16
10
|
Response,
|
|
11
|
+
Router,
|
|
17
12
|
Timeout,
|
|
13
|
+
Token,
|
|
18
14
|
TooManyRequests,
|
|
19
15
|
Unauthorized,
|
|
20
16
|
WebSocketRoute,
|
|
@@ -22,8 +18,6 @@ import {
|
|
|
22
18
|
contentTypes,
|
|
23
19
|
convertPathToURL,
|
|
24
20
|
createLogger,
|
|
25
|
-
isAuthorized,
|
|
26
|
-
isConnected,
|
|
27
21
|
queryParamsToObject,
|
|
28
22
|
readBody,
|
|
29
23
|
shimLegacyRequests,
|
|
@@ -32,7 +26,6 @@ import {
|
|
|
32
26
|
|
|
33
27
|
// @ts-ignore
|
|
34
28
|
import Enjoi from 'enjoi';
|
|
35
|
-
import micromatch from 'micromatch';
|
|
36
29
|
|
|
37
30
|
export interface HTTPServerOptions {
|
|
38
31
|
concurrent: number;
|
|
@@ -43,26 +36,20 @@ export interface HTTPServerOptions {
|
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
export class HTTPServer {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
protected server: http.Server = http.createServer();
|
|
40
|
+
protected port: number;
|
|
41
|
+
protected host?: string;
|
|
42
|
+
protected log = createLogger('server');
|
|
43
|
+
protected verbose = createLogger('server:verbose');
|
|
51
44
|
|
|
52
45
|
constructor(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
private httpRoutes: Array<HTTPRoute | BrowserHTTPRoute>,
|
|
58
|
-
private webSocketRoutes: Array<WebSocketRoute | BrowserWebsocketRoute>,
|
|
46
|
+
protected config: Config,
|
|
47
|
+
protected metrics: Metrics,
|
|
48
|
+
protected token: Token,
|
|
49
|
+
protected router: Router,
|
|
59
50
|
) {
|
|
60
51
|
this.host = config.getHost();
|
|
61
52
|
this.port = config.getPort();
|
|
62
|
-
this.httpRoutes = httpRoutes.map((r) => this.registerHTTPRoute(r));
|
|
63
|
-
this.webSocketRoutes = webSocketRoutes.map((r) =>
|
|
64
|
-
this.registerWebSocketRoute(r),
|
|
65
|
-
);
|
|
66
53
|
|
|
67
54
|
this.log(
|
|
68
55
|
`Server instantiated with host "${this.host}" on port "${
|
|
@@ -71,33 +58,16 @@ export class HTTPServer {
|
|
|
71
58
|
);
|
|
72
59
|
}
|
|
73
60
|
|
|
74
|
-
|
|
75
|
-
this.log(`Queue is full, sending 429 response`);
|
|
76
|
-
return writeResponse(res, 429, 'Too many requests');
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
private onQueueFullWebSocket = (_req: Request, socket: stream.Duplex) => {
|
|
80
|
-
this.log(`Queue is full, sending 429 response`);
|
|
81
|
-
return writeResponse(socket, 429, 'Too many requests');
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
private onHTTPTimeout = (_req: Request, res: Response) => {
|
|
85
|
-
this.log(`HTTP job has timedout, sending 429 response`);
|
|
86
|
-
return writeResponse(res, 408, 'Request has timed out');
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
private onWebsocketTimeout = (_req: Request, socket: stream.Duplex) => {
|
|
90
|
-
this.log(`Websocket job has timedout, sending 429 response`);
|
|
91
|
-
return writeResponse(socket, 408, 'Request has timed out');
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
private onHTTPUnauthorized = (_req: Request, res: Response) => {
|
|
61
|
+
protected onHTTPUnauthorized = (_req: Request, res: Response) => {
|
|
95
62
|
this.log(`HTTP request is not properly authorized, responding with 401`);
|
|
96
63
|
this.metrics.addUnauthorized();
|
|
97
64
|
return writeResponse(res, 401, 'Bad or missing authentication.');
|
|
98
65
|
};
|
|
99
66
|
|
|
100
|
-
|
|
67
|
+
protected onWebsocketUnauthorized = (
|
|
68
|
+
_req: Request,
|
|
69
|
+
socket: stream.Duplex,
|
|
70
|
+
) => {
|
|
101
71
|
this.log(
|
|
102
72
|
`Websocket request is not properly authorized, responding with 401`,
|
|
103
73
|
);
|
|
@@ -105,141 +75,6 @@ export class HTTPServer {
|
|
|
105
75
|
return writeResponse(socket, 401, 'Bad or missing authentication.');
|
|
106
76
|
};
|
|
107
77
|
|
|
108
|
-
private wrapHTTPHandler =
|
|
109
|
-
(
|
|
110
|
-
route: HTTPRoute | BrowserHTTPRoute,
|
|
111
|
-
handler: HTTPRoute['handler'] | BrowserHTTPRoute['handler'],
|
|
112
|
-
) =>
|
|
113
|
-
async (req: Request, res: Response) => {
|
|
114
|
-
if (!isConnected(res)) {
|
|
115
|
-
this.log(`HTTP Request has closed prior to running`);
|
|
116
|
-
return Promise.resolve();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (route.browser) {
|
|
120
|
-
const browser = await this.browserManager.getBrowserForRequest(
|
|
121
|
-
req,
|
|
122
|
-
route,
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
if (!isConnected(res)) {
|
|
126
|
-
this.log(`HTTP Request has closed prior to running`);
|
|
127
|
-
this.browserManager.complete(browser);
|
|
128
|
-
return Promise.resolve();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (!browser) {
|
|
132
|
-
return writeResponse(res, 500, `Error loading the browser.`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!isConnected(res)) {
|
|
136
|
-
this.log(`HTTP Request has closed prior to running`);
|
|
137
|
-
return Promise.resolve();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
this.verbose(`Running found HTTP handler.`);
|
|
142
|
-
return await handler(req, res, browser);
|
|
143
|
-
} finally {
|
|
144
|
-
this.verbose(`HTTP Request handler has finished.`);
|
|
145
|
-
this.browserManager.complete(browser);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return (handler as HTTPRoute['handler'])(req, res);
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
private wrapWebSocketHandler =
|
|
153
|
-
(
|
|
154
|
-
route: WebSocketRoute | BrowserWebsocketRoute,
|
|
155
|
-
handler: WebSocketRoute['handler'] | BrowserWebsocketRoute['handler'],
|
|
156
|
-
) =>
|
|
157
|
-
async (req: Request, socket: stream.Duplex, head: Buffer) => {
|
|
158
|
-
if (!isConnected(socket)) {
|
|
159
|
-
this.log(`WebSocket Request has closed prior to running`);
|
|
160
|
-
return Promise.resolve();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (route.browser) {
|
|
164
|
-
const browser = await this.browserManager.getBrowserForRequest(
|
|
165
|
-
req,
|
|
166
|
-
route,
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
if (!isConnected(socket)) {
|
|
170
|
-
this.log(`WebSocket Request has closed prior to running`);
|
|
171
|
-
this.browserManager.complete(browser);
|
|
172
|
-
return Promise.resolve();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (!browser) {
|
|
176
|
-
return writeResponse(socket, 500, `Error loading the browser.`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
this.verbose(`Running found WebSocket handler.`);
|
|
181
|
-
await handler(req, socket, head, browser);
|
|
182
|
-
} finally {
|
|
183
|
-
this.verbose(`WebSocket Request handler has finished.`);
|
|
184
|
-
this.browserManager.complete(browser);
|
|
185
|
-
}
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
return (handler as WebSocketRoute['handler'])(req, socket, head);
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
private getTimeout(req: Request) {
|
|
192
|
-
const timer = req.parsed.searchParams.get('timeout');
|
|
193
|
-
|
|
194
|
-
return timer ? +timer : undefined;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private registerHTTPRoute(
|
|
198
|
-
route: HTTPRoute | BrowserHTTPRoute,
|
|
199
|
-
): HTTPRoute | BrowserHTTPRoute {
|
|
200
|
-
this.verbose(
|
|
201
|
-
`Registering HTTP ${route.method.toUpperCase()} ${route.path}`,
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
route._browserManager = () => this.browserManager;
|
|
205
|
-
|
|
206
|
-
const bound = route.handler.bind(route);
|
|
207
|
-
const wrapped = this.wrapHTTPHandler(route, bound);
|
|
208
|
-
|
|
209
|
-
route.handler = route.concurrency
|
|
210
|
-
? this.limiter.limit(
|
|
211
|
-
wrapped,
|
|
212
|
-
this.onQueueFullHTTP,
|
|
213
|
-
this.onHTTPTimeout,
|
|
214
|
-
this.getTimeout,
|
|
215
|
-
)
|
|
216
|
-
: wrapped;
|
|
217
|
-
|
|
218
|
-
return route;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private registerWebSocketRoute(
|
|
222
|
-
route: WebSocketRoute | BrowserWebsocketRoute,
|
|
223
|
-
): WebSocketRoute | BrowserWebsocketRoute {
|
|
224
|
-
this.verbose(`Registering WebSocket "${route.path}"`);
|
|
225
|
-
|
|
226
|
-
route._browserManager = () => this.browserManager;
|
|
227
|
-
|
|
228
|
-
const bound = route.handler.bind(route);
|
|
229
|
-
const wrapped = this.wrapWebSocketHandler(route, bound);
|
|
230
|
-
|
|
231
|
-
route.handler = route.concurrency
|
|
232
|
-
? this.limiter.limit(
|
|
233
|
-
wrapped,
|
|
234
|
-
this.onQueueFullWebSocket,
|
|
235
|
-
this.onWebsocketTimeout,
|
|
236
|
-
this.getTimeout,
|
|
237
|
-
)
|
|
238
|
-
: wrapped;
|
|
239
|
-
|
|
240
|
-
return route;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
78
|
public async start(): Promise<void> {
|
|
244
79
|
this.log(`HTTP Server is starting`);
|
|
245
80
|
|
|
@@ -268,21 +103,19 @@ export class HTTPServer {
|
|
|
268
103
|
public async stop(): Promise<void> {
|
|
269
104
|
this.log(`HTTP Server is shutting down`);
|
|
270
105
|
await new Promise((r) => this.server.close(r));
|
|
271
|
-
await Promise.all([this.tearDown(), this.
|
|
106
|
+
await Promise.all([this.tearDown(), this.router.teardown()]);
|
|
272
107
|
this.log(`HTTP Server shutdown complete`);
|
|
273
108
|
}
|
|
274
109
|
|
|
275
|
-
|
|
110
|
+
protected tearDown() {
|
|
276
111
|
this.log(`Tearing down all listeners and internal routes`);
|
|
277
112
|
this.server && this.server.removeAllListeners();
|
|
278
|
-
this.httpRoutes = [];
|
|
279
|
-
this.webSocketRoutes = [];
|
|
280
113
|
|
|
281
114
|
// @ts-ignore garbage collect this reference
|
|
282
115
|
this.server = null;
|
|
283
116
|
}
|
|
284
117
|
|
|
285
|
-
|
|
118
|
+
protected handleRequest = async (
|
|
286
119
|
request: http.IncomingMessage,
|
|
287
120
|
res: http.ServerResponse,
|
|
288
121
|
) => {
|
|
@@ -297,10 +130,6 @@ export class HTTPServer {
|
|
|
297
130
|
|
|
298
131
|
if (!proceed) return;
|
|
299
132
|
|
|
300
|
-
const staticHandler = this.httpRoutes.find(
|
|
301
|
-
(route) => route.path === HTTPManagementRoutes.static,
|
|
302
|
-
) as HTTPRoute;
|
|
303
|
-
|
|
304
133
|
if (this.config.getAllowCORS()) {
|
|
305
134
|
Object.entries(this.config.getCORSHeaders()).forEach(([header, value]) =>
|
|
306
135
|
res.setHeader(header, value),
|
|
@@ -323,36 +152,19 @@ export class HTTPServer {
|
|
|
323
152
|
req.parsed.searchParams.delete('body');
|
|
324
153
|
}
|
|
325
154
|
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
| undefined;
|
|
330
|
-
|
|
331
|
-
const found =
|
|
332
|
-
this.httpRoutes.find(
|
|
333
|
-
(r) =>
|
|
334
|
-
micromatch.isMatch(req.parsed.pathname, r.path) &&
|
|
335
|
-
r.method === (req.method?.toLocaleLowerCase() as Methods) &&
|
|
336
|
-
(accepts.some((a) => a.startsWith('*/*')) ||
|
|
337
|
-
r.contentTypes.some((contentType) =>
|
|
338
|
-
accepts.includes(contentType),
|
|
339
|
-
)) &&
|
|
340
|
-
((!contentType && r.accepts.includes(contentTypes.any)) ||
|
|
341
|
-
r.accepts.includes(contentType as contentTypes)),
|
|
342
|
-
) || (req.method?.toLowerCase() === 'get' ? staticHandler : null);
|
|
343
|
-
|
|
344
|
-
if (!found) {
|
|
155
|
+
const route = await this.router.getRouteForHTTPRequest(req);
|
|
156
|
+
|
|
157
|
+
if (!route) {
|
|
345
158
|
this.log(`No matching WebSocket route handler for "${req.parsed.href}"`);
|
|
346
159
|
writeResponse(res, 404, 'Not Found');
|
|
347
160
|
return Promise.resolve();
|
|
348
161
|
}
|
|
349
162
|
|
|
350
|
-
this.verbose(`Found matching HTTP route handler "${
|
|
163
|
+
this.verbose(`Found matching HTTP route handler "${route.path}"`);
|
|
351
164
|
|
|
352
|
-
if (
|
|
165
|
+
if (route?.auth) {
|
|
353
166
|
this.verbose(`Authorizing HTTP request to "${request.url}"`);
|
|
354
|
-
const
|
|
355
|
-
const isPermitted = isAuthorized(req, found, tokens);
|
|
167
|
+
const isPermitted = await this.token.isAuthorized(req, route);
|
|
356
168
|
|
|
357
169
|
if (!isPermitted) {
|
|
358
170
|
return this.onHTTPUnauthorized(req, res);
|
|
@@ -365,8 +177,8 @@ export class HTTPServer {
|
|
|
365
177
|
|
|
366
178
|
if (
|
|
367
179
|
((req.headers['content-type']?.includes(contentTypes.json) ||
|
|
368
|
-
(
|
|
369
|
-
|
|
180
|
+
(route.accepts.length === 1 &&
|
|
181
|
+
route.accepts.includes(contentTypes.json))) &&
|
|
370
182
|
typeof body !== 'object') ||
|
|
371
183
|
body === null
|
|
372
184
|
) {
|
|
@@ -374,10 +186,10 @@ export class HTTPServer {
|
|
|
374
186
|
return Promise.resolve();
|
|
375
187
|
}
|
|
376
188
|
|
|
377
|
-
if (
|
|
189
|
+
if (route.querySchema) {
|
|
378
190
|
this.verbose(`Validating route query-params with QUERY schema`);
|
|
379
191
|
try {
|
|
380
|
-
const schema = Enjoi.schema(
|
|
192
|
+
const schema = Enjoi.schema(route.querySchema);
|
|
381
193
|
const valid = schema.validate(req.queryParams, {
|
|
382
194
|
abortEarly: false,
|
|
383
195
|
});
|
|
@@ -419,10 +231,10 @@ export class HTTPServer {
|
|
|
419
231
|
}
|
|
420
232
|
}
|
|
421
233
|
|
|
422
|
-
if (
|
|
234
|
+
if (route.bodySchema) {
|
|
423
235
|
this.verbose(`Validating route payload with BODY schema`);
|
|
424
236
|
try {
|
|
425
|
-
const schema = Enjoi.schema(
|
|
237
|
+
const schema = Enjoi.schema(route.bodySchema);
|
|
426
238
|
const valid = schema.validate(body, { abortEarly: false });
|
|
427
239
|
|
|
428
240
|
if (valid.error) {
|
|
@@ -460,9 +272,7 @@ export class HTTPServer {
|
|
|
460
272
|
}
|
|
461
273
|
}
|
|
462
274
|
|
|
463
|
-
|
|
464
|
-
// argument for this to to work properly
|
|
465
|
-
return (found as HTTPRoute)
|
|
275
|
+
return (route as HTTPRoute)
|
|
466
276
|
.handler(req, res)
|
|
467
277
|
.then(() => {
|
|
468
278
|
this.verbose('HTTP connection complete');
|
|
@@ -488,12 +298,12 @@ export class HTTPServer {
|
|
|
488
298
|
return writeResponse(res, 408, e.message);
|
|
489
299
|
}
|
|
490
300
|
|
|
491
|
-
this.log(`Error handling request at "${
|
|
301
|
+
this.log(`Error handling request at "${route.path}": ${e}`);
|
|
492
302
|
return writeResponse(res, 500, e.toString());
|
|
493
303
|
});
|
|
494
304
|
};
|
|
495
305
|
|
|
496
|
-
|
|
306
|
+
protected handleWebSocket = async (
|
|
497
307
|
request: http.IncomingMessage,
|
|
498
308
|
socket: stream.Duplex,
|
|
499
309
|
head: Buffer,
|
|
@@ -507,29 +317,26 @@ export class HTTPServer {
|
|
|
507
317
|
|
|
508
318
|
if (!proceed) return;
|
|
509
319
|
|
|
510
|
-
const { pathname } = req.parsed;
|
|
511
320
|
req.queryParams = queryParamsToObject(req.parsed.searchParams);
|
|
512
321
|
|
|
513
|
-
const
|
|
514
|
-
micromatch.isMatch(pathname, r.path),
|
|
515
|
-
);
|
|
322
|
+
const route = await this.router.getRouteForWebSocketRequest(req);
|
|
516
323
|
|
|
517
|
-
if (
|
|
518
|
-
this.verbose(`Found matching WebSocket route handler "${
|
|
324
|
+
if (route) {
|
|
325
|
+
this.verbose(`Found matching WebSocket route handler "${route.path}"`);
|
|
519
326
|
|
|
520
|
-
if (
|
|
327
|
+
if (route?.auth) {
|
|
521
328
|
this.verbose(`Authorizing WebSocket request to "${req.parsed.href}"`);
|
|
522
|
-
const isPermitted = isAuthorized(req,
|
|
329
|
+
const isPermitted = await this.token.isAuthorized(req, route);
|
|
523
330
|
|
|
524
331
|
if (!isPermitted) {
|
|
525
332
|
return this.onWebsocketUnauthorized(req, socket);
|
|
526
333
|
}
|
|
527
334
|
}
|
|
528
335
|
|
|
529
|
-
if (
|
|
336
|
+
if (route.querySchema) {
|
|
530
337
|
this.verbose(`Validating route query-params with QUERY schema`);
|
|
531
338
|
try {
|
|
532
|
-
const schema = Enjoi.schema(
|
|
339
|
+
const schema = Enjoi.schema(route.querySchema);
|
|
533
340
|
const valid = schema.validate(req.queryParams, {
|
|
534
341
|
abortEarly: false,
|
|
535
342
|
});
|
|
@@ -571,9 +378,7 @@ export class HTTPServer {
|
|
|
571
378
|
}
|
|
572
379
|
}
|
|
573
380
|
|
|
574
|
-
|
|
575
|
-
// argument for this to to work properly
|
|
576
|
-
return (found as WebSocketRoute)
|
|
381
|
+
return (route as WebSocketRoute)
|
|
577
382
|
.handler(req, socket, head)
|
|
578
383
|
.then(() => {
|
|
579
384
|
this.verbose('Websocket connection complete');
|
|
@@ -596,7 +401,7 @@ export class HTTPServer {
|
|
|
596
401
|
}
|
|
597
402
|
|
|
598
403
|
this.log(
|
|
599
|
-
`Error handling request at "${
|
|
404
|
+
`Error handling request at "${route.path}": ${e}\n${e.stack}`,
|
|
600
405
|
);
|
|
601
406
|
|
|
602
407
|
return writeResponse(socket, 500, e.message);
|
package/src/token.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BrowserHTTPRoute,
|
|
3
|
+
BrowserWebsocketRoute,
|
|
4
|
+
Config,
|
|
5
|
+
HTTPRoute,
|
|
6
|
+
Request,
|
|
7
|
+
WebSocketRoute,
|
|
8
|
+
getTokenFromRequest,
|
|
9
|
+
} from '@browserless.io/browserless';
|
|
10
|
+
|
|
11
|
+
export class Token {
|
|
12
|
+
constructor(protected config: Config) {}
|
|
13
|
+
|
|
14
|
+
public isAuthorized = async (
|
|
15
|
+
req: Request,
|
|
16
|
+
route:
|
|
17
|
+
| BrowserHTTPRoute
|
|
18
|
+
| BrowserWebsocketRoute
|
|
19
|
+
| HTTPRoute
|
|
20
|
+
| WebSocketRoute,
|
|
21
|
+
): Promise<boolean> => {
|
|
22
|
+
const token = this.config.getToken();
|
|
23
|
+
|
|
24
|
+
if (token === null) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (route.auth !== true) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const requestToken = getTokenFromRequest(req);
|
|
33
|
+
|
|
34
|
+
if (!requestToken) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (Array.isArray(token) ? token : [token]).includes(requestToken);
|
|
39
|
+
};
|
|
40
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -2,15 +2,13 @@ import * as http from 'http';
|
|
|
2
2
|
import * as stream from 'stream';
|
|
3
3
|
import {
|
|
4
4
|
APITags,
|
|
5
|
-
|
|
5
|
+
Browserless,
|
|
6
6
|
CDPChromium,
|
|
7
7
|
Config,
|
|
8
|
-
FileSystem,
|
|
9
8
|
HTTPManagementRoutes,
|
|
10
9
|
HTTPRoutes,
|
|
11
10
|
Methods,
|
|
12
11
|
Metrics,
|
|
13
|
-
Monitoring,
|
|
14
12
|
PlaywrightChromium,
|
|
15
13
|
PlaywrightFirefox,
|
|
16
14
|
PlaywrightWebkit,
|
|
@@ -81,54 +79,104 @@ export interface BrowserJSON {
|
|
|
81
79
|
'WebKit-Version': string;
|
|
82
80
|
}
|
|
83
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The default launch options or a function, accepting
|
|
84
|
+
* the request object, that produces the launch options.
|
|
85
|
+
*/
|
|
84
86
|
type defaultLaunchOptions =
|
|
85
87
|
| CDPLaunchOptions
|
|
86
88
|
| BrowserlessLaunch
|
|
87
89
|
| ((req: Request) => CDPLaunchOptions | BrowserlessLaunch);
|
|
88
90
|
|
|
89
91
|
interface Route {
|
|
90
|
-
_browserManager?: () => BrowserManager;
|
|
91
|
-
_config?: () => Config;
|
|
92
|
-
_debug?: () => debug.Debugger;
|
|
93
|
-
_fileSystem?: () => FileSystem;
|
|
94
|
-
_metrics?: () => Metrics;
|
|
95
|
-
_monitor?: () => Monitoring;
|
|
96
|
-
|
|
97
92
|
/**
|
|
98
|
-
*
|
|
93
|
+
* A boolean, or a function that returns a boolean, on
|
|
94
|
+
* whether the route requires an API token to access.
|
|
99
95
|
*/
|
|
100
96
|
auth: boolean | ((req: Request) => Promise<boolean>);
|
|
101
97
|
|
|
102
98
|
/**
|
|
103
99
|
* The schematic of the submitted BODY (typically)
|
|
104
|
-
* an object when the route is json-based.
|
|
100
|
+
* an object when the route is json-based. This is generated
|
|
101
|
+
* automatically if your route defines a BodySchema type.
|
|
105
102
|
*/
|
|
106
103
|
bodySchema?: unknown;
|
|
107
104
|
|
|
108
105
|
/**
|
|
109
106
|
* Whether the route should be bound by the global
|
|
110
|
-
* concurrency limit
|
|
107
|
+
* concurrency limit defined in your configuration.
|
|
111
108
|
*/
|
|
112
109
|
concurrency: boolean;
|
|
113
110
|
|
|
114
111
|
/**
|
|
115
|
-
* Description of the route and what it does
|
|
112
|
+
* Description of the route and what it does. This description
|
|
113
|
+
* is then used in the embedded documentation site.
|
|
116
114
|
*/
|
|
117
115
|
description: string;
|
|
118
116
|
|
|
119
117
|
/**
|
|
120
|
-
*
|
|
118
|
+
* Helper function to load the browser-manager instance. Defined
|
|
119
|
+
* and injected by browserless after initialization.
|
|
120
|
+
* @returns BrowserManager
|
|
121
|
+
*/
|
|
122
|
+
getBrowserManager?: () => Browserless['browserManager'];
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Helper function that loads the config module. Defined and injected by
|
|
127
|
+
* browserless after initialization.
|
|
128
|
+
* @returns Config
|
|
129
|
+
*/
|
|
130
|
+
getConfig?: () => Browserless['config'];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Helper function that loads the debug module, useful
|
|
134
|
+
* for logging messages scoped to the routes path. Defined
|
|
135
|
+
* and injected by browserless after initialization.
|
|
136
|
+
* @returns Debug
|
|
137
|
+
*/
|
|
138
|
+
getDebug?: () => debug.Debugger;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Helper function that loads the file-system module
|
|
142
|
+
* for interacting with file-systems. Defined and injected by
|
|
143
|
+
* browserless after initialization.
|
|
144
|
+
* @returns FileSystem
|
|
145
|
+
*/
|
|
146
|
+
getFileSystem?: () => Browserless['fileSystem'];
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Helper function that loads the metrics module for
|
|
150
|
+
* collecting and aggregating statistics. Defined and injected by
|
|
151
|
+
* browserless after initialization.
|
|
152
|
+
* @returns Metrics
|
|
153
|
+
*/
|
|
154
|
+
getMetrics?: () => Browserless['metrics'];
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Helper function that loads the monitoring module useful
|
|
158
|
+
* for monitoring system health. Defined and injected by
|
|
159
|
+
* browserless after initialization.
|
|
160
|
+
* @returns Monitor
|
|
161
|
+
*/
|
|
162
|
+
getMonitoring?: () => Browserless['monitoring'];
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The HTTP path that this route handles, eg '/my-route'
|
|
121
166
|
*/
|
|
122
167
|
path: HTTPRoutes | WebsocketRoutes | HTTPManagementRoutes | string;
|
|
123
168
|
|
|
124
169
|
/**
|
|
125
170
|
* The query parameters accepted by the route, defined in
|
|
126
|
-
* an object format.
|
|
171
|
+
* an object format. This is auto-generated for you if your
|
|
172
|
+
* route defines and exports a QuerySchema type.
|
|
127
173
|
*/
|
|
128
174
|
querySchema?: unknown;
|
|
129
175
|
|
|
130
176
|
/**
|
|
131
|
-
* The structure of the routes response when successful
|
|
177
|
+
* The structure of the routes response when successful. This
|
|
178
|
+
* is auto-generated for you if your route defines a ResponseSchema
|
|
179
|
+
* type and exports it in your route.
|
|
132
180
|
*/
|
|
133
181
|
responseSchema?: unknown;
|
|
134
182
|
|
|
@@ -139,6 +187,11 @@ interface Route {
|
|
|
139
187
|
tags: APITags[];
|
|
140
188
|
}
|
|
141
189
|
|
|
190
|
+
/**
|
|
191
|
+
* A primitive HTTP-based route that doesn't require a
|
|
192
|
+
* browser in order to fulfill requests. Used by downstream HTTPRoute
|
|
193
|
+
* and WebSocketRoute
|
|
194
|
+
*/
|
|
142
195
|
interface BasicHTTPRoute extends Route {
|
|
143
196
|
/**
|
|
144
197
|
* The allowed Content-Types that this route can read and handle.
|
|
@@ -160,6 +213,10 @@ interface BasicHTTPRoute extends Route {
|
|
|
160
213
|
method: Methods;
|
|
161
214
|
}
|
|
162
215
|
|
|
216
|
+
/**
|
|
217
|
+
* A HTTP-based route, with a handler, that can fulfill requests without
|
|
218
|
+
* a browser required.
|
|
219
|
+
*/
|
|
163
220
|
export interface HTTPRoute extends BasicHTTPRoute {
|
|
164
221
|
browser: null;
|
|
165
222
|
|
|
@@ -169,6 +226,11 @@ export interface HTTPRoute extends BasicHTTPRoute {
|
|
|
169
226
|
handler: (req: Request, res: http.ServerResponse) => Promise<unknown>;
|
|
170
227
|
}
|
|
171
228
|
|
|
229
|
+
/**
|
|
230
|
+
* A HTTP-based route, with a handler, that can fulfill requests but
|
|
231
|
+
* requires a browser in order to do so. Handler will then be called
|
|
232
|
+
* with a 3rd argument of the browser class specified.
|
|
233
|
+
*/
|
|
172
234
|
export interface BrowserHTTPRoute extends BasicHTTPRoute {
|
|
173
235
|
browser: BrowserClasses;
|
|
174
236
|
|
|
@@ -187,6 +249,10 @@ export interface BrowserHTTPRoute extends BasicHTTPRoute {
|
|
|
187
249
|
onNewPage?: undefined;
|
|
188
250
|
}
|
|
189
251
|
|
|
252
|
+
/**
|
|
253
|
+
* A WebSocket-based route, with a handler, that can fulfill requests
|
|
254
|
+
* that do not require a browser in order to operate.
|
|
255
|
+
*/
|
|
190
256
|
export interface WebSocketRoute extends Route {
|
|
191
257
|
browser: null;
|
|
192
258
|
|
|
@@ -200,6 +266,11 @@ export interface WebSocketRoute extends Route {
|
|
|
200
266
|
) => Promise<unknown>;
|
|
201
267
|
}
|
|
202
268
|
|
|
269
|
+
/**
|
|
270
|
+
* A WebSocket-based route, with a handler, that can fulfill requests
|
|
271
|
+
* that need a browser. Handler is called with an additional argument of
|
|
272
|
+
* browser (the browser class required to run the route).
|
|
273
|
+
*/
|
|
203
274
|
export interface BrowserWebsocketRoute extends Route {
|
|
204
275
|
browser: BrowserClasses;
|
|
205
276
|
|
|
@@ -216,6 +287,10 @@ export interface BrowserWebsocketRoute extends Route {
|
|
|
216
287
|
browser: BrowserInstance,
|
|
217
288
|
): Promise<unknown>;
|
|
218
289
|
|
|
290
|
+
/**
|
|
291
|
+
* An optional function to automatically set up or handle new page
|
|
292
|
+
* creation. Useful for injecting behaviors or other functionality.
|
|
293
|
+
*/
|
|
219
294
|
onNewPage?: (url: URL, page: Page) => Promise<void>;
|
|
220
295
|
}
|
|
221
296
|
|