@push.rocks/smartpuppeteer 2.1.0 → 2.3.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.
@@ -2,10 +2,18 @@ import * as plugins from './smartpuppeteer.plugins.js';
2
2
 
3
3
  export interface IEnvAwareOptions {
4
4
  forceNoSandbox?: boolean;
5
+ requireSandbox?: boolean;
5
6
  usePipe?: boolean;
6
7
  launchOptions?: plugins.puppeteer.LaunchOptions;
7
8
  }
8
9
 
10
+ const sandboxDisablingArguments = [
11
+ '--disable-gpu-sandbox',
12
+ '--disable-seccomp-filter-sandbox',
13
+ '--disable-setuid-sandbox',
14
+ '--no-sandbox',
15
+ ];
16
+
9
17
  export const resolveBrowserExecutablePath = (
10
18
  candidateNamesArg: string[] = [
11
19
  'google-chrome',
@@ -29,15 +37,30 @@ export const getEnvAwareBrowserInstance = async (
29
37
  ): Promise<plugins.puppeteer.Browser> => {
30
38
  const options: IEnvAwareOptions = {
31
39
  forceNoSandbox: false,
40
+ requireSandbox: false,
32
41
  ...optionsArg,
33
42
  };
34
43
 
44
+ if (options.forceNoSandbox && options.requireSandbox) {
45
+ throw new Error('forceNoSandbox and requireSandbox are mutually exclusive');
46
+ }
47
+
35
48
  const launchOptions = options.launchOptions ?? {};
36
49
  let chromeArgs: string[] = [...(launchOptions.args ?? [])];
37
- if (
38
- process.env.CI ||
39
- options.forceNoSandbox ||
40
- plugins.os.userInfo().username === 'root'
50
+ if (options.requireSandbox) {
51
+ const forbiddenArgument = chromeArgs.find((argument) => (
52
+ sandboxDisablingArguments.includes(argument.split('=', 1)[0]!)
53
+ ));
54
+ if (forbiddenArgument) {
55
+ throw new Error(`Sandbox-required browser launch rejects argument: ${forbiddenArgument}`);
56
+ }
57
+ if (process.platform === 'linux' && process.getuid?.() === 0) {
58
+ throw new Error('Sandbox-required Chromium cannot be launched as root');
59
+ }
60
+ } else if (
61
+ process.env.CI
62
+ || options.forceNoSandbox
63
+ || plugins.os.userInfo().username === 'root'
41
64
  ) {
42
65
  for (const sandboxArg of ['--no-sandbox', '--disable-setuid-sandbox']) {
43
66
  if (!chromeArgs.includes(sandboxArg)) {
@@ -0,0 +1,169 @@
1
+ import * as plugins from './smartpuppeteer.plugins.js';
2
+
3
+ export interface IOwnedProcessIdentity {
4
+ pid: number;
5
+ parentPid: number;
6
+ processGroupId: number;
7
+ sessionId: number;
8
+ startTime: string;
9
+ state: string;
10
+ }
11
+
12
+ const maxOwnedGroupProcesses = 4096;
13
+ const maxVisibleProcesses = 65536;
14
+ const processReadBatchSize = 64;
15
+
16
+ const parseProcessStat = (pid: number, stat: string): IOwnedProcessIdentity => {
17
+ const commandEnd = stat.lastIndexOf(')');
18
+ if (commandEnd < 0) {
19
+ throw new Error(`Unable to parse process identity for PID ${pid}`);
20
+ }
21
+ const fields = stat.slice(commandEnd + 2).trim().split(/\s+/);
22
+ const state = fields[0];
23
+ const parentPid = Number(fields[1]);
24
+ const processGroupId = Number(fields[2]);
25
+ const sessionId = Number(fields[3]);
26
+ const startTime = fields[19];
27
+ if (
28
+ typeof state !== 'string'
29
+ || state.length !== 1
30
+ || !Number.isInteger(parentPid)
31
+ || parentPid < 0
32
+ || !Number.isInteger(processGroupId)
33
+ || processGroupId < 0
34
+ || !Number.isInteger(sessionId)
35
+ || sessionId < 0
36
+ || typeof startTime !== 'string'
37
+ || !/^[0-9]+$/.test(startTime)
38
+ ) {
39
+ throw new Error(`Unable to parse process identity for PID ${pid}`);
40
+ }
41
+ return { pid, parentPid, processGroupId, sessionId, startTime, state };
42
+ };
43
+
44
+ export const readOwnedProcessIdentity = async (
45
+ pid: number,
46
+ ): Promise<IOwnedProcessIdentity | undefined> => {
47
+ if (process.platform !== 'linux') {
48
+ throw new Error('Confirmed browser process ownership is supported on Linux only');
49
+ }
50
+ try {
51
+ const stat = await plugins.fs.promises.readFile(`/proc/${pid}/stat`, 'utf8');
52
+ return parseProcessStat(pid, stat);
53
+ } catch (error) {
54
+ if (
55
+ (error as NodeJS.ErrnoException).code === 'ENOENT'
56
+ || (error as NodeJS.ErrnoException).code === 'ESRCH'
57
+ ) {
58
+ return undefined;
59
+ }
60
+ throw error;
61
+ }
62
+ };
63
+
64
+ const listProcessIdentities = async (): Promise<IOwnedProcessIdentity[]> => {
65
+ const entries = await plugins.fs.promises.readdir('/proc', { withFileTypes: true });
66
+ const pids = entries
67
+ .filter((entry) => entry.isDirectory() && /^[0-9]+$/.test(entry.name))
68
+ .map((entry) => Number(entry.name));
69
+ if (pids.length > maxVisibleProcesses) {
70
+ throw new Error(`Refusing to inspect more than ${maxVisibleProcesses} processes`);
71
+ }
72
+ const identities: Array<IOwnedProcessIdentity | undefined> = [];
73
+ for (let index = 0; index < pids.length; index += processReadBatchSize) {
74
+ identities.push(...await Promise.all(
75
+ pids.slice(index, index + processReadBatchSize).map((pid) => readOwnedProcessIdentity(pid)),
76
+ ));
77
+ }
78
+ return identities.filter((identity): identity is IOwnedProcessIdentity => Boolean(identity));
79
+ };
80
+
81
+ export const listOwnedProcessGroupMembers = async (
82
+ rootIdentity: IOwnedProcessIdentity,
83
+ ): Promise<IOwnedProcessIdentity[]> => {
84
+ const currentRoot = await readOwnedProcessIdentity(rootIdentity.pid);
85
+ if (currentRoot && currentRoot.startTime !== rootIdentity.startTime) {
86
+ throw new Error('The owned browser PID was reused by another process');
87
+ }
88
+ if (
89
+ currentRoot
90
+ && (
91
+ currentRoot.processGroupId !== rootIdentity.processGroupId
92
+ || currentRoot.sessionId !== rootIdentity.sessionId
93
+ )
94
+ ) {
95
+ throw new Error('The owned browser process changed its process-group identity');
96
+ }
97
+ const members = (await listProcessIdentities()).filter((identity) => (
98
+ identity.processGroupId === rootIdentity.processGroupId
99
+ && identity.sessionId === rootIdentity.sessionId
100
+ ));
101
+ if (members.length > maxOwnedGroupProcesses) {
102
+ throw new Error(`Owned browser process group exceeded ${maxOwnedGroupProcesses} processes`);
103
+ }
104
+ return members;
105
+ };
106
+
107
+ export const killFrozenOwnedProcessGroup = (rootIdentity: IOwnedProcessIdentity): boolean => {
108
+ try {
109
+ process.kill(-rootIdentity.processGroupId, 'SIGKILL');
110
+ return true;
111
+ } catch (error) {
112
+ if ((error as NodeJS.ErrnoException).code === 'ESRCH') {
113
+ return false;
114
+ }
115
+ throw error;
116
+ }
117
+ };
118
+
119
+ export const signalOwnedProcessGroup = async (
120
+ rootIdentity: IOwnedProcessIdentity,
121
+ signal: NodeJS.Signals,
122
+ ): Promise<boolean> => {
123
+ const currentRoot = await readOwnedProcessIdentity(rootIdentity.pid);
124
+ if (!currentRoot) {
125
+ return false;
126
+ }
127
+ if (
128
+ currentRoot.startTime !== rootIdentity.startTime
129
+ || currentRoot.processGroupId !== rootIdentity.processGroupId
130
+ || currentRoot.sessionId !== rootIdentity.sessionId
131
+ ) {
132
+ throw new Error('Refusing to signal a reused browser process-group leader');
133
+ }
134
+ try {
135
+ process.kill(-rootIdentity.processGroupId, signal);
136
+ return true;
137
+ } catch (error) {
138
+ if ((error as NodeJS.ErrnoException).code === 'ESRCH') {
139
+ return false;
140
+ }
141
+ throw error;
142
+ }
143
+ };
144
+
145
+ export const signalOwnedProcessIdentity = async (
146
+ identity: IOwnedProcessIdentity,
147
+ signal: NodeJS.Signals,
148
+ ): Promise<boolean> => {
149
+ const currentIdentity = await readOwnedProcessIdentity(identity.pid);
150
+ if (!currentIdentity) {
151
+ return false;
152
+ }
153
+ if (currentIdentity.startTime !== identity.startTime) {
154
+ throw new Error(`Refusing to signal reused PID ${identity.pid}`);
155
+ }
156
+ try {
157
+ process.kill(identity.pid, signal);
158
+ return true;
159
+ } catch (error) {
160
+ if ((error as NodeJS.ErrnoException).code === 'ESRCH') {
161
+ return false;
162
+ }
163
+ throw error;
164
+ }
165
+ };
166
+
167
+ export const delay = async (milliseconds: number): Promise<void> => {
168
+ await new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
169
+ };
@@ -23,6 +23,42 @@ export interface ILiveBrowserScreencastOptions {
23
23
  everyNthFrame?: number;
24
24
  }
25
25
 
26
+ export interface ILiveBrowserSecurityOptions {
27
+ denyDownloads?: boolean;
28
+ denyFileChoosers?: boolean;
29
+ denyPermissions?: boolean;
30
+ httpNavigationOnly?: boolean;
31
+ proxyCredentials?: ILiveBrowserProxyCredentials;
32
+ }
33
+
34
+ export interface ILiveBrowserProxyCredentials {
35
+ username: string;
36
+ password: string;
37
+ }
38
+
39
+ export interface ILiveBrowserOperationOptions {
40
+ signal?: AbortSignal;
41
+ }
42
+
43
+ export type TLiveBrowserJsonValue =
44
+ | null
45
+ | boolean
46
+ | number
47
+ | string
48
+ | TLiveBrowserJsonValue[]
49
+ | { [key: string]: TLiveBrowserJsonValue };
50
+
51
+ export interface ILiveBrowserEvaluateOptions {
52
+ tabId?: string;
53
+ timeoutMs?: number;
54
+ maxOutputBytes?: number;
55
+ maxDepth?: number;
56
+ maxNodes?: number;
57
+ maxStringBytes?: number;
58
+ maxArrayLength?: number;
59
+ maxObjectKeys?: number;
60
+ }
61
+
26
62
  export type TLiveBrowserLaunchOptions = Omit<
27
63
  NonNullable<IEnvAwareOptions['launchOptions']>,
28
64
  'signal'
@@ -32,6 +68,29 @@ export interface ILiveBrowserSessionOptions extends Omit<IEnvAwareOptions, 'laun
32
68
  launchOptions?: TLiveBrowserLaunchOptions;
33
69
  viewport?: ILiveBrowserViewport;
34
70
  screencast?: ILiveBrowserScreencastOptions;
71
+ security?: ILiveBrowserSecurityOptions;
72
+ allowEvaluation?: boolean;
73
+ }
74
+
75
+ export interface ILiveBrowserProcessState {
76
+ generation: number;
77
+ pid: number | null;
78
+ processGroupId: number | null;
79
+ running: boolean;
80
+ exitCode: number | null;
81
+ signalCode: NodeJS.Signals | null;
82
+ }
83
+
84
+ export interface ILiveBrowserTerminationOptions {
85
+ gracefulTimeoutMs?: number;
86
+ forceTimeoutMs?: number;
87
+ }
88
+
89
+ export interface ILiveBrowserTerminationResult extends ILiveBrowserProcessState {
90
+ forced: boolean;
91
+ shutdownComplete: boolean;
92
+ confirmedDead: boolean;
93
+ errors: string[];
35
94
  }
36
95
 
37
96
  export interface ILiveBrowserTabState {
@@ -1,8 +1,10 @@
1
1
  // node native scope
2
2
  import { Buffer } from 'node:buffer';
3
- import * as os from 'os';
3
+ import * as fs from 'node:fs';
4
+ import * as http from 'node:http';
5
+ import * as os from 'node:os';
4
6
 
5
- export { Buffer, os };
7
+ export { Buffer, fs, http, os };
6
8
 
7
9
  // @pushrocks scope
8
10
  import * as smartdelay from '@push.rocks/smartdelay';