@appium/base-driver 10.2.1 → 10.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.
- package/build/lib/basedriver/commands/execute.js +2 -1
- package/build/lib/basedriver/commands/execute.js.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts +42 -78
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.js +87 -139
- package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts +52 -97
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +98 -141
- package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
- package/lib/basedriver/commands/execute.ts +2 -1
- package/lib/jsonwp-proxy/protocol-converter.ts +284 -0
- package/lib/jsonwp-proxy/proxy.ts +506 -0
- package/package.json +9 -9
- package/lib/jsonwp-proxy/protocol-converter.js +0 -317
- package/lib/jsonwp-proxy/proxy.js +0 -493
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppiumLogger,
|
|
3
|
+
HTTPBody,
|
|
4
|
+
HTTPHeaders,
|
|
5
|
+
HTTPMethod,
|
|
6
|
+
ProxyOptions,
|
|
7
|
+
ProxyResponse,
|
|
8
|
+
} from '@appium/types';
|
|
9
|
+
import _ from 'lodash';
|
|
10
|
+
import {logger, util} from '@appium/support';
|
|
11
|
+
import {getSummaryByCode} from '../jsonwp-status/status';
|
|
12
|
+
import {
|
|
13
|
+
errors,
|
|
14
|
+
isErrorType,
|
|
15
|
+
errorFromMJSONWPStatusCode,
|
|
16
|
+
errorFromW3CJsonCode,
|
|
17
|
+
getResponseForW3CError,
|
|
18
|
+
} from '../protocol/errors';
|
|
19
|
+
import {isSessionCommand, routeToCommandName} from '../protocol';
|
|
20
|
+
import {MAX_LOG_BODY_LENGTH, DEFAULT_BASE_PATH, PROTOCOLS} from '../constants';
|
|
21
|
+
import {ProtocolConverter} from './protocol-converter';
|
|
22
|
+
import {formatResponseValue, ensureW3cResponse} from '../protocol/helpers';
|
|
23
|
+
import http from 'node:http';
|
|
24
|
+
import https from 'node:https';
|
|
25
|
+
import {match as pathToRegexMatch} from 'path-to-regexp';
|
|
26
|
+
import nodeUrl from 'node:url';
|
|
27
|
+
import {ProxyRequest} from './proxy-request';
|
|
28
|
+
import type {Request, Response} from 'express';
|
|
29
|
+
import type {AxiosError, AxiosResponse, RawAxiosRequestConfig} from 'axios';
|
|
30
|
+
|
|
31
|
+
const DEFAULT_LOG = logger.getLogger('WD Proxy');
|
|
32
|
+
const DEFAULT_REQUEST_TIMEOUT = 240000;
|
|
33
|
+
const COMMAND_WITH_SESSION_ID_MATCHER = pathToRegexMatch(
|
|
34
|
+
'{/*prefix}/session/:sessionId{/*command}'
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const {MJSONWP, W3C} = PROTOCOLS;
|
|
38
|
+
|
|
39
|
+
type Protocol = (typeof PROTOCOLS)[keyof typeof PROTOCOLS];
|
|
40
|
+
|
|
41
|
+
const ALLOWED_OPTS = [
|
|
42
|
+
'scheme',
|
|
43
|
+
'server',
|
|
44
|
+
'port',
|
|
45
|
+
'base',
|
|
46
|
+
'reqBasePath',
|
|
47
|
+
'sessionId',
|
|
48
|
+
'timeout',
|
|
49
|
+
'log',
|
|
50
|
+
'keepAlive',
|
|
51
|
+
'headers',
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
export class JWProxy {
|
|
55
|
+
readonly scheme: string;
|
|
56
|
+
readonly server: string;
|
|
57
|
+
readonly port: number;
|
|
58
|
+
readonly base: string;
|
|
59
|
+
readonly reqBasePath: string;
|
|
60
|
+
sessionId: string | null;
|
|
61
|
+
readonly timeout: number;
|
|
62
|
+
readonly headers: HTTPHeaders | undefined;
|
|
63
|
+
readonly httpAgent: http.Agent;
|
|
64
|
+
readonly httpsAgent: https.Agent;
|
|
65
|
+
readonly protocolConverter: ProtocolConverter;
|
|
66
|
+
|
|
67
|
+
private _downstreamProtocol: Protocol | null | undefined;
|
|
68
|
+
private _activeRequests: ProxyRequest[];
|
|
69
|
+
private readonly _log: AppiumLogger | undefined;
|
|
70
|
+
|
|
71
|
+
constructor(opts: ProxyOptions = {}) {
|
|
72
|
+
const filteredOpts = _.pick(opts, ALLOWED_OPTS);
|
|
73
|
+
const options = _.defaults(_.omit(filteredOpts, 'log'), {
|
|
74
|
+
scheme: 'http',
|
|
75
|
+
server: 'localhost',
|
|
76
|
+
port: 4444,
|
|
77
|
+
base: DEFAULT_BASE_PATH,
|
|
78
|
+
reqBasePath: DEFAULT_BASE_PATH,
|
|
79
|
+
sessionId: null,
|
|
80
|
+
timeout: DEFAULT_REQUEST_TIMEOUT,
|
|
81
|
+
}) as ProxyOptions & {
|
|
82
|
+
scheme: string;
|
|
83
|
+
server: string;
|
|
84
|
+
port: number;
|
|
85
|
+
base: string;
|
|
86
|
+
reqBasePath: string;
|
|
87
|
+
sessionId: string | null;
|
|
88
|
+
timeout: number;
|
|
89
|
+
};
|
|
90
|
+
options.scheme = options.scheme.toLowerCase();
|
|
91
|
+
Object.assign(this, options);
|
|
92
|
+
|
|
93
|
+
this._activeRequests = [];
|
|
94
|
+
this._downstreamProtocol = null;
|
|
95
|
+
const agentOpts = {
|
|
96
|
+
keepAlive: opts.keepAlive ?? true,
|
|
97
|
+
maxSockets: 10,
|
|
98
|
+
maxFreeSockets: 5,
|
|
99
|
+
};
|
|
100
|
+
this.httpAgent = new http.Agent(agentOpts);
|
|
101
|
+
this.httpsAgent = new https.Agent(agentOpts);
|
|
102
|
+
this.protocolConverter = new ProtocolConverter(this.proxy.bind(this), opts.log);
|
|
103
|
+
this._log = opts.log;
|
|
104
|
+
|
|
105
|
+
this.log.debug(`${this.constructor.name} options: ${JSON.stringify(options)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get log(): AppiumLogger {
|
|
109
|
+
return this._log ?? DEFAULT_LOG;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns the number of active downstream HTTP requests.
|
|
114
|
+
*/
|
|
115
|
+
getActiveRequestsCount(): number {
|
|
116
|
+
return this._activeRequests.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Cancels all currently active downstream HTTP requests.
|
|
121
|
+
*/
|
|
122
|
+
cancelActiveRequests(): void {
|
|
123
|
+
for (const ar of this._activeRequests) {
|
|
124
|
+
ar.cancel();
|
|
125
|
+
}
|
|
126
|
+
this._activeRequests = [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Sets the protocol used by the downstream server (W3C or MJSONWP).
|
|
131
|
+
*/
|
|
132
|
+
set downstreamProtocol(value: Protocol | null | undefined) {
|
|
133
|
+
this._downstreamProtocol = value;
|
|
134
|
+
this.protocolConverter.downstreamProtocol = value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Gets the protocol used by the downstream server (W3C or MJSONWP).
|
|
139
|
+
*/
|
|
140
|
+
get downstreamProtocol(): Protocol | null | undefined {
|
|
141
|
+
return this._downstreamProtocol;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Builds a full downstream URL (including base path and session) for a given upstream URL.
|
|
146
|
+
*/
|
|
147
|
+
getUrlForProxy(url: string, method?: HTTPMethod): string {
|
|
148
|
+
const parsedUrl = this._parseUrl(url);
|
|
149
|
+
const normalizedPathname = this._toNormalizedPathname(parsedUrl);
|
|
150
|
+
const commandName = normalizedPathname
|
|
151
|
+
? routeToCommandName(normalizedPathname, method)
|
|
152
|
+
: '';
|
|
153
|
+
const requiresSessionId =
|
|
154
|
+
!commandName || (commandName && isSessionCommand(commandName));
|
|
155
|
+
const proxyPrefix = `${this.scheme}://${this.server}:${this.port}${this.base}`;
|
|
156
|
+
let proxySuffix = normalizedPathname ? `/${_.trimStart(normalizedPathname, '/')}` : '';
|
|
157
|
+
if (parsedUrl.search) {
|
|
158
|
+
proxySuffix += parsedUrl.search;
|
|
159
|
+
}
|
|
160
|
+
if (!requiresSessionId) {
|
|
161
|
+
return `${proxyPrefix}${proxySuffix}`;
|
|
162
|
+
}
|
|
163
|
+
if (!this.sessionId) {
|
|
164
|
+
throw new ReferenceError(
|
|
165
|
+
`Session ID is not set, but saw a URL that requires it (${url})`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return `${proxyPrefix}/session/${this.sessionId}${proxySuffix}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Proxies a raw WebDriver command to the downstream server.
|
|
173
|
+
*/
|
|
174
|
+
async proxy(
|
|
175
|
+
url: string,
|
|
176
|
+
method: string,
|
|
177
|
+
body: HTTPBody = null
|
|
178
|
+
): Promise<[ProxyResponse, HTTPBody]> {
|
|
179
|
+
method = method.toUpperCase();
|
|
180
|
+
const newUrl = this.getUrlForProxy(url, method as HTTPMethod);
|
|
181
|
+
const truncateBody = (content: unknown): string =>
|
|
182
|
+
_.truncate(_.isString(content) ? content : JSON.stringify(content), {
|
|
183
|
+
length: MAX_LOG_BODY_LENGTH,
|
|
184
|
+
});
|
|
185
|
+
const reqOpts: RawAxiosRequestConfig = {
|
|
186
|
+
url: newUrl,
|
|
187
|
+
method,
|
|
188
|
+
headers: {
|
|
189
|
+
'content-type': 'application/json; charset=utf-8',
|
|
190
|
+
'user-agent': 'appium',
|
|
191
|
+
accept: 'application/json, */*',
|
|
192
|
+
...(this.headers ?? {}),
|
|
193
|
+
},
|
|
194
|
+
proxy: false,
|
|
195
|
+
timeout: this.timeout,
|
|
196
|
+
httpAgent: this.httpAgent,
|
|
197
|
+
httpsAgent: this.httpsAgent,
|
|
198
|
+
};
|
|
199
|
+
// GET methods shouldn't have any body. Most servers are OK with this,
|
|
200
|
+
// but WebDriverAgent throws 400 errors
|
|
201
|
+
if (util.hasValue(body) && method !== 'GET') {
|
|
202
|
+
if (typeof body !== 'object') {
|
|
203
|
+
try {
|
|
204
|
+
reqOpts.data = JSON.parse(body as string);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
this.log.warn(
|
|
207
|
+
'Invalid body payload (%s): %s',
|
|
208
|
+
(error as Error).message,
|
|
209
|
+
logger.markSensitive(truncateBody(body))
|
|
210
|
+
);
|
|
211
|
+
throw new Error(
|
|
212
|
+
'Cannot interpret the request body as valid JSON. Check the server log for more details.'
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
reqOpts.data = body;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.log.debug(
|
|
221
|
+
`Proxying [%s %s] to [%s %s] with ${reqOpts.data ? 'body: %s' : '%s body'}`,
|
|
222
|
+
method,
|
|
223
|
+
url || '/',
|
|
224
|
+
method,
|
|
225
|
+
newUrl,
|
|
226
|
+
reqOpts.data ? logger.markSensitive(truncateBody(reqOpts.data)) : 'no'
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const throwProxyError = (error: unknown): never => {
|
|
230
|
+
const err = new Error(`The request to ${url} has failed`) as Error & {
|
|
231
|
+
response: {data: unknown; status: number};
|
|
232
|
+
};
|
|
233
|
+
err.response = {
|
|
234
|
+
data: error,
|
|
235
|
+
status: 500,
|
|
236
|
+
};
|
|
237
|
+
throw err;
|
|
238
|
+
};
|
|
239
|
+
let isResponseLogged = false;
|
|
240
|
+
try {
|
|
241
|
+
const {data, status, headers} = await this.request(reqOpts);
|
|
242
|
+
// `data` might be really big
|
|
243
|
+
// Be careful while handling it to avoid memory leaks
|
|
244
|
+
if (!_.isPlainObject(data)) {
|
|
245
|
+
// The response should be a valid JSON object
|
|
246
|
+
// If it cannot be coerced to an object then the response is wrong
|
|
247
|
+
throwProxyError(data);
|
|
248
|
+
}
|
|
249
|
+
this.log.debug(`Got response with status ${status}: ${truncateBody(data)}`);
|
|
250
|
+
isResponseLogged = true;
|
|
251
|
+
const isSessionCreationRequest = url.endsWith('/session') && method === 'POST';
|
|
252
|
+
if (isSessionCreationRequest) {
|
|
253
|
+
if (status === 200) {
|
|
254
|
+
const value = (data as Record<string, unknown>).value as
|
|
255
|
+
| Record<string, unknown>
|
|
256
|
+
| undefined;
|
|
257
|
+
const raw =
|
|
258
|
+
(data as Record<string, unknown>).sessionId ?? value?.sessionId;
|
|
259
|
+
this.sessionId =
|
|
260
|
+
typeof raw === 'string' ? raw : raw != null ? String(raw) : null;
|
|
261
|
+
}
|
|
262
|
+
this.downstreamProtocol = this.getProtocolFromResBody(
|
|
263
|
+
data as Record<string, unknown>
|
|
264
|
+
) ?? this.downstreamProtocol;
|
|
265
|
+
this.log.info(`Determined the downstream protocol as '${this.downstreamProtocol}'`);
|
|
266
|
+
}
|
|
267
|
+
if (
|
|
268
|
+
_.has(data, 'status') &&
|
|
269
|
+
parseInt((data as Record<string, unknown>).status as string, 10) !== 0
|
|
270
|
+
) {
|
|
271
|
+
throwProxyError(data);
|
|
272
|
+
}
|
|
273
|
+
return [
|
|
274
|
+
{
|
|
275
|
+
statusCode: status,
|
|
276
|
+
headers: headers as HTTPHeaders,
|
|
277
|
+
body: data,
|
|
278
|
+
},
|
|
279
|
+
data,
|
|
280
|
+
];
|
|
281
|
+
} catch (e: unknown) {
|
|
282
|
+
const err = e as AxiosError<unknown> & {message: string};
|
|
283
|
+
let proxyErrorMsg = err.message;
|
|
284
|
+
if (util.hasValue(err.response)) {
|
|
285
|
+
if (!isResponseLogged) {
|
|
286
|
+
const error = truncateBody(err.response.data);
|
|
287
|
+
this.log.info(
|
|
288
|
+
util.hasValue(err.response.status)
|
|
289
|
+
? `Got response with status ${err.response.status}: ${error}`
|
|
290
|
+
: `Got response with unknown status: ${error}`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
proxyErrorMsg = `Could not proxy command to the remote server. Original error: ${err.message}`;
|
|
295
|
+
this.log.info(err.message);
|
|
296
|
+
}
|
|
297
|
+
throw new errors.ProxyRequestError(
|
|
298
|
+
proxyErrorMsg,
|
|
299
|
+
err.response?.data,
|
|
300
|
+
err.response?.status
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Detects the downstream protocol from a response body.
|
|
307
|
+
*/
|
|
308
|
+
getProtocolFromResBody(resObj: Record<string, unknown>): Protocol | undefined {
|
|
309
|
+
if (_.isInteger(resObj.status)) {
|
|
310
|
+
return MJSONWP;
|
|
311
|
+
}
|
|
312
|
+
if (!_.isUndefined(resObj.value)) {
|
|
313
|
+
return W3C;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Proxies a command identified by its HTTP method and URL to the downstream server.
|
|
319
|
+
*/
|
|
320
|
+
async proxyCommand(
|
|
321
|
+
url: string,
|
|
322
|
+
method: HTTPMethod,
|
|
323
|
+
body: HTTPBody = null
|
|
324
|
+
): Promise<[ProxyResponse, HTTPBody]> {
|
|
325
|
+
const parsedUrl = this._parseUrl(url);
|
|
326
|
+
const normalizedPathname = this._toNormalizedPathname(parsedUrl);
|
|
327
|
+
const commandName = normalizedPathname
|
|
328
|
+
? routeToCommandName(normalizedPathname, method)
|
|
329
|
+
: '';
|
|
330
|
+
if (!commandName) {
|
|
331
|
+
return await this.proxy(url, method, body);
|
|
332
|
+
}
|
|
333
|
+
this.log.debug(`Matched '${url}' to command name '${commandName}'`);
|
|
334
|
+
|
|
335
|
+
return await this.protocolConverter.convertAndProxy(commandName, url, method, body);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Executes a WebDriver command and returns the unwrapped `value` field (or throws).
|
|
340
|
+
*/
|
|
341
|
+
async command(
|
|
342
|
+
url: string,
|
|
343
|
+
method: HTTPMethod,
|
|
344
|
+
body: HTTPBody = null
|
|
345
|
+
): Promise<HTTPBody> {
|
|
346
|
+
let response: ProxyResponse;
|
|
347
|
+
let resBodyObj: HTTPBody;
|
|
348
|
+
try {
|
|
349
|
+
[response, resBodyObj] = await this.proxyCommand(url, method, body);
|
|
350
|
+
} catch (err: unknown) {
|
|
351
|
+
if (isErrorType(err, errors.ProxyRequestError)) {
|
|
352
|
+
throw err.getActualError();
|
|
353
|
+
}
|
|
354
|
+
throw new errors.UnknownError((err as Error).message);
|
|
355
|
+
}
|
|
356
|
+
const resBody = resBodyObj as Record<string, unknown>;
|
|
357
|
+
const protocol = this.getProtocolFromResBody(resBody);
|
|
358
|
+
if (protocol === MJSONWP) {
|
|
359
|
+
if (response.statusCode === 200 && resBody.status === 0) {
|
|
360
|
+
return resBody.value;
|
|
361
|
+
}
|
|
362
|
+
const status = parseInt(resBody.status as string, 10);
|
|
363
|
+
if (!isNaN(status) && status !== 0) {
|
|
364
|
+
let message: unknown = resBody.value;
|
|
365
|
+
if (_.isPlainObject(message) && _.has(message, 'message')) {
|
|
366
|
+
message = (message as Record<string, unknown>).message;
|
|
367
|
+
}
|
|
368
|
+
throw errorFromMJSONWPStatusCode(status, _.isEmpty(message)
|
|
369
|
+
? getSummaryByCode(status)
|
|
370
|
+
: (message as string | {message: string}));
|
|
371
|
+
}
|
|
372
|
+
} else if (protocol === W3C) {
|
|
373
|
+
if (response.statusCode < 300) {
|
|
374
|
+
return resBody.value;
|
|
375
|
+
}
|
|
376
|
+
if (_.isPlainObject(resBody.value) && (resBody.value as Record<string, unknown>).error) {
|
|
377
|
+
const value = resBody.value as Record<string, unknown>;
|
|
378
|
+
throw errorFromW3CJsonCode(
|
|
379
|
+
value.error as string,
|
|
380
|
+
(value.message as string) ?? '',
|
|
381
|
+
value.stacktrace as string | undefined
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
} else if (response.statusCode === 200) {
|
|
385
|
+
return resBodyObj;
|
|
386
|
+
}
|
|
387
|
+
throw new errors.UnknownError(
|
|
388
|
+
`Did not know what to do with response code '${response.statusCode}' ` +
|
|
389
|
+
`and response body '${_.truncate(JSON.stringify(resBodyObj), {
|
|
390
|
+
length: 300,
|
|
391
|
+
})}'`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Extracts a session id from a WebDriver-style URL.
|
|
397
|
+
*/
|
|
398
|
+
getSessionIdFromUrl(url: string): string | null {
|
|
399
|
+
const match = url.match(/\/session\/([^/]+)/);
|
|
400
|
+
return match ? match[1] : null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Proxies an Express `Request`/`Response` pair to the downstream server,
|
|
405
|
+
* converting any downstream errors into a proper W3C HTTP response.
|
|
406
|
+
*
|
|
407
|
+
* This method must not throw; it always writes a response.
|
|
408
|
+
*/
|
|
409
|
+
async proxyReqRes(req: Request, res: Response): Promise<void> {
|
|
410
|
+
let statusCode: number;
|
|
411
|
+
let resBodyObj: HTTPBody;
|
|
412
|
+
try {
|
|
413
|
+
const [response, body] = await this.proxyCommand(
|
|
414
|
+
req.originalUrl,
|
|
415
|
+
req.method as HTTPMethod,
|
|
416
|
+
req.body
|
|
417
|
+
);
|
|
418
|
+
statusCode = response.statusCode;
|
|
419
|
+
resBodyObj = body;
|
|
420
|
+
} catch (err: unknown) {
|
|
421
|
+
[statusCode, resBodyObj] = getResponseForW3CError(
|
|
422
|
+
isErrorType(err, errors.ProxyRequestError) ? (err as InstanceType<typeof errors.ProxyRequestError>).getActualError() : err
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
426
|
+
if (!_.isPlainObject(resBodyObj)) {
|
|
427
|
+
const error = new errors.UnknownError(
|
|
428
|
+
`The downstream server response with the status code ${statusCode} is not a valid JSON object: ` +
|
|
429
|
+
_.truncate(`${resBodyObj}`, {length: 300})
|
|
430
|
+
);
|
|
431
|
+
[statusCode, resBodyObj] = getResponseForW3CError(error);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const resBody = resBodyObj as Record<string, unknown>;
|
|
435
|
+
if (_.has(resBody, 'sessionId')) {
|
|
436
|
+
const reqSessionId = this.getSessionIdFromUrl(req.originalUrl);
|
|
437
|
+
if (reqSessionId) {
|
|
438
|
+
this.log.info(`Replacing sessionId ${resBody.sessionId} with ${reqSessionId}`);
|
|
439
|
+
resBody.sessionId = reqSessionId;
|
|
440
|
+
} else if (this.sessionId) {
|
|
441
|
+
this.log.info(`Replacing sessionId ${resBody.sessionId} with ${this.sessionId}`);
|
|
442
|
+
resBody.sessionId = this.sessionId;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
resBody.value = formatResponseValue(resBody.value as object | undefined);
|
|
446
|
+
res.status(statusCode).json(ensureW3cResponse(resBody));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Performs requests to the downstream server
|
|
451
|
+
*
|
|
452
|
+
* @private - Do not call this method directly,
|
|
453
|
+
* it uses client-specific arguments and responses!
|
|
454
|
+
*/
|
|
455
|
+
private async request(requestConfig: RawAxiosRequestConfig): Promise<AxiosResponse> {
|
|
456
|
+
const req = new ProxyRequest(requestConfig);
|
|
457
|
+
this._activeRequests.push(req);
|
|
458
|
+
try {
|
|
459
|
+
return await req.execute();
|
|
460
|
+
} finally {
|
|
461
|
+
_.pull(this._activeRequests, req);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private _parseUrl(url: string): nodeUrl.UrlWithStringQuery {
|
|
466
|
+
// eslint-disable-next-line n/no-deprecated-api -- we need relative URL support
|
|
467
|
+
const parsedUrl = nodeUrl.parse(url || '/');
|
|
468
|
+
if (
|
|
469
|
+
_.isNil(parsedUrl.href) ||
|
|
470
|
+
_.isNil(parsedUrl.pathname) ||
|
|
471
|
+
(parsedUrl.protocol && !['http:', 'https:'].includes(parsedUrl.protocol))
|
|
472
|
+
) {
|
|
473
|
+
throw new Error(`Did not know how to proxy the url '${url}'`);
|
|
474
|
+
}
|
|
475
|
+
return parsedUrl;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private _toNormalizedPathname(parsedUrl: nodeUrl.UrlWithStringQuery): string {
|
|
479
|
+
if (!_.isString(parsedUrl.pathname)) {
|
|
480
|
+
return '';
|
|
481
|
+
}
|
|
482
|
+
let pathname =
|
|
483
|
+
this.reqBasePath && parsedUrl.pathname.startsWith(this.reqBasePath)
|
|
484
|
+
? parsedUrl.pathname.replace(this.reqBasePath, '')
|
|
485
|
+
: parsedUrl.pathname;
|
|
486
|
+
const match = COMMAND_WITH_SESSION_ID_MATCHER(pathname);
|
|
487
|
+
// This is needed for the backward compatibility
|
|
488
|
+
// if drivers don't set reqBasePath properly
|
|
489
|
+
if (!this.reqBasePath) {
|
|
490
|
+
if (match && match.params && _.isArray((match.params as Record<string, unknown>).prefix)) {
|
|
491
|
+
pathname = pathname.replace(
|
|
492
|
+
`/${((match.params as Record<string, unknown>).prefix as string[]).join('/')}`,
|
|
493
|
+
''
|
|
494
|
+
);
|
|
495
|
+
} else if (_.startsWith(pathname, '/wd/hub')) {
|
|
496
|
+
pathname = pathname.replace('/wd/hub', '');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
let result = pathname;
|
|
500
|
+
if (match && match.params) {
|
|
501
|
+
const command = (match.params as Record<string, unknown>).command;
|
|
502
|
+
result = _.isArray(command) ? `/${(command as string[]).join('/')}` : '';
|
|
503
|
+
}
|
|
504
|
+
return _.trimEnd(result, '/');
|
|
505
|
+
}
|
|
506
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appium/base-driver",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.3.0",
|
|
4
4
|
"description": "Base driver class for Appium drivers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"automation",
|
|
@@ -45,24 +45,24 @@
|
|
|
45
45
|
"test:types": "tsd && tsc -p tsconfig.test.json"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@appium/support": "
|
|
49
|
-
"@appium/types": "
|
|
48
|
+
"@appium/support": "7.1.0",
|
|
49
|
+
"@appium/types": "1.3.0",
|
|
50
50
|
"@colors/colors": "1.6.0",
|
|
51
51
|
"async-lock": "1.4.1",
|
|
52
52
|
"asyncbox": "6.1.0",
|
|
53
|
-
"axios": "1.
|
|
53
|
+
"axios": "1.15.0",
|
|
54
54
|
"bluebird": "3.7.2",
|
|
55
55
|
"body-parser": "2.2.2",
|
|
56
56
|
"express": "5.2.1",
|
|
57
57
|
"fastest-levenshtein": "1.0.16",
|
|
58
58
|
"http-status-codes": "2.3.0",
|
|
59
|
-
"lodash": "4.
|
|
60
|
-
"lru-cache": "11.
|
|
59
|
+
"lodash": "4.18.1",
|
|
60
|
+
"lru-cache": "11.3.3",
|
|
61
61
|
"method-override": "3.0.0",
|
|
62
62
|
"morgan": "1.10.1",
|
|
63
|
-
"path-to-regexp": "8.
|
|
63
|
+
"path-to-regexp": "8.4.2",
|
|
64
64
|
"serve-favicon": "2.5.1",
|
|
65
|
-
"type-fest": "5.
|
|
65
|
+
"type-fest": "5.5.0"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
68
|
"spdy": "4.0.2"
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"publishConfig": {
|
|
75
75
|
"access": "public"
|
|
76
76
|
},
|
|
77
|
-
"gitHead": "
|
|
77
|
+
"gitHead": "7a8965f5c30ffec2ad04ce75903b3960537cef06",
|
|
78
78
|
"tsd": {
|
|
79
79
|
"directory": "test/types"
|
|
80
80
|
}
|