@appium/base-driver 10.0.0-beta.0 → 10.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/README.md +0 -8
- package/build/lib/basedriver/capabilities.d.ts.map +1 -1
- package/build/lib/basedriver/capabilities.js +2 -4
- package/build/lib/basedriver/capabilities.js.map +1 -1
- package/build/lib/basedriver/commands/execute.js.map +1 -1
- package/build/lib/basedriver/commands/timeout.js +12 -31
- package/build/lib/basedriver/commands/timeout.js.map +1 -1
- package/build/lib/basedriver/core.d.ts +0 -8
- package/build/lib/basedriver/core.d.ts.map +1 -1
- package/build/lib/basedriver/core.js +8 -18
- package/build/lib/basedriver/core.js.map +1 -1
- package/build/lib/basedriver/driver.js +2 -2
- package/build/lib/basedriver/driver.js.map +1 -1
- package/build/lib/basedriver/helpers.d.ts +9 -1
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +56 -142
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/basedriver/validation.d.ts +7 -0
- package/build/lib/basedriver/validation.d.ts.map +1 -0
- package/build/lib/basedriver/validation.js +130 -0
- package/build/lib/basedriver/validation.js.map +1 -0
- package/build/lib/express/middleware.d.ts +0 -6
- package/build/lib/express/middleware.d.ts.map +1 -1
- package/build/lib/express/middleware.js +28 -60
- package/build/lib/express/middleware.js.map +1 -1
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +0 -1
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/helpers/capabilities.d.ts +13 -6
- package/build/lib/helpers/capabilities.d.ts.map +1 -1
- package/build/lib/helpers/capabilities.js +7 -0
- package/build/lib/helpers/capabilities.js.map +1 -1
- package/build/lib/index.d.ts +1 -0
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +3 -1
- package/build/lib/index.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts +0 -8
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +7 -38
- package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
- package/build/lib/protocol/errors.d.ts +171 -277
- package/build/lib/protocol/errors.d.ts.map +1 -1
- package/build/lib/protocol/errors.js +201 -421
- package/build/lib/protocol/errors.js.map +1 -1
- package/build/lib/protocol/helpers.d.ts +6 -6
- package/build/lib/protocol/helpers.d.ts.map +1 -1
- package/build/lib/protocol/helpers.js +11 -7
- package/build/lib/protocol/helpers.js.map +1 -1
- package/build/lib/protocol/index.d.ts +2 -1
- package/build/lib/protocol/index.d.ts.map +1 -1
- package/build/lib/protocol/index.js +2 -1
- package/build/lib/protocol/index.js.map +1 -1
- package/build/lib/protocol/protocol.d.ts +16 -19
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +98 -119
- package/build/lib/protocol/protocol.js.map +1 -1
- package/build/lib/protocol/routes.d.ts +12 -714
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +24 -488
- package/build/lib/protocol/routes.js.map +1 -1
- package/build/lib/protocol/validators.d.ts +4 -7
- package/build/lib/protocol/validators.d.ts.map +1 -1
- package/build/lib/protocol/validators.js +4 -21
- package/build/lib/protocol/validators.js.map +1 -1
- package/lib/basedriver/capabilities.ts +2 -4
- package/lib/basedriver/commands/execute.ts +1 -1
- package/lib/basedriver/commands/timeout.ts +16 -43
- package/lib/basedriver/core.ts +10 -19
- package/lib/basedriver/driver.ts +3 -3
- package/lib/basedriver/helpers.js +61 -167
- package/lib/basedriver/validation.ts +145 -0
- package/lib/express/middleware.js +32 -70
- package/lib/express/server.js +0 -2
- package/lib/helpers/capabilities.js +9 -4
- package/lib/index.js +2 -0
- package/lib/jsonwp-proxy/proxy.js +8 -45
- package/lib/protocol/{errors.js → errors.ts} +322 -436
- package/lib/protocol/helpers.js +12 -8
- package/lib/protocol/index.js +8 -1
- package/lib/protocol/{protocol.js → protocol.ts} +147 -146
- package/lib/protocol/routes.js +26 -498
- package/lib/protocol/validators.ts +19 -0
- package/package.json +10 -11
- package/build/lib/basedriver/desired-caps.d.ts +0 -5
- package/build/lib/basedriver/desired-caps.d.ts.map +0 -1
- package/build/lib/basedriver/desired-caps.js +0 -92
- package/build/lib/basedriver/desired-caps.js.map +0 -1
- package/lib/basedriver/README.md +0 -36
- package/lib/basedriver/desired-caps.js +0 -103
- package/lib/express/README.md +0 -59
- package/lib/jsonwp-proxy/README.md +0 -52
- package/lib/jsonwp-status/README.md +0 -20
- package/lib/protocol/README.md +0 -100
- package/lib/protocol/validators.js +0 -38
|
@@ -2,7 +2,7 @@ import _ from 'lodash';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import url from 'url';
|
|
4
4
|
import logger from './logger';
|
|
5
|
-
import {tempDir, fs, util,
|
|
5
|
+
import {tempDir, fs, util, timing, node} from '@appium/support';
|
|
6
6
|
import { LRUCache } from 'lru-cache';
|
|
7
7
|
import AsyncLock from 'async-lock';
|
|
8
8
|
import axios from 'axios';
|
|
@@ -10,9 +10,6 @@ import B from 'bluebird';
|
|
|
10
10
|
|
|
11
11
|
// for compat with running tests transpiled and in-place
|
|
12
12
|
export const {version: BASEDRIVER_VER} = fs.readPackageJsonFrom(__dirname);
|
|
13
|
-
const IPA_EXT = '.ipa';
|
|
14
|
-
const ZIP_EXTS = new Set(['.zip', IPA_EXT]);
|
|
15
|
-
const ZIP_MIME_TYPES = ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'];
|
|
16
13
|
const CACHED_APPS_MAX_AGE_MS = 1000 * 60 * toNaturalNumber(60 * 24, 'APPIUM_APPS_CACHE_MAX_AGE');
|
|
17
14
|
const MAX_CACHED_APPS = toNaturalNumber(1024, 'APPIUM_APPS_CACHE_MAX_ITEMS');
|
|
18
15
|
const HTTP_STATUS_NOT_MODIFIED = 304;
|
|
@@ -62,9 +59,17 @@ process.on('exit', () => {
|
|
|
62
59
|
});
|
|
63
60
|
|
|
64
61
|
/**
|
|
62
|
+
* Perform inital application package configuration
|
|
63
|
+
* to prepare it for the further consumption by a driver:
|
|
64
|
+
*
|
|
65
|
+
* - Manages caching logic
|
|
66
|
+
* - Downloads the app from a remote URL to the local filesystem
|
|
67
|
+
* - Determines package name
|
|
68
|
+
* - Checks basic requiremenets on the application package
|
|
65
69
|
*
|
|
66
70
|
* @param {string} app
|
|
67
71
|
* @param {string|string[]|import('@appium/types').ConfigureAppOptions} options
|
|
72
|
+
* @returns {Promise<string>}
|
|
68
73
|
*/
|
|
69
74
|
export async function configureApp(
|
|
70
75
|
app,
|
|
@@ -72,9 +77,10 @@ export async function configureApp(
|
|
|
72
77
|
) {
|
|
73
78
|
if (!_.isString(app)) {
|
|
74
79
|
// immediately shortcircuit if not given an app
|
|
75
|
-
return;
|
|
80
|
+
return '';
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
/** @type {string[]} */
|
|
78
84
|
let supportedAppExtensions;
|
|
79
85
|
const onPostProcess = !_.isString(options) && !_.isArray(options) ? options.onPostProcess : undefined;
|
|
80
86
|
const onDownload = !_.isString(options) && !_.isArray(options) ? options.onDownload : undefined;
|
|
@@ -86,13 +92,13 @@ export async function configureApp(
|
|
|
86
92
|
} else if (_.isPlainObject(options)) {
|
|
87
93
|
supportedAppExtensions = options.supportedExtensions;
|
|
88
94
|
}
|
|
95
|
+
// @ts-ignore this is OK
|
|
89
96
|
if (_.isEmpty(supportedAppExtensions)) {
|
|
90
97
|
throw new Error(`One or more supported app extensions must be provided`);
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
let newApp = app;
|
|
94
101
|
const originalAppLink = app;
|
|
95
|
-
let shouldUnzipApp = false;
|
|
96
102
|
let packageHash = null;
|
|
97
103
|
/** @type {import('axios').AxiosResponse['headers']|undefined} */
|
|
98
104
|
let headers = undefined;
|
|
@@ -172,67 +178,19 @@ export async function configureApp(
|
|
|
172
178
|
({stream, headers, status} = await queryAppLink(newApp, {...DEFAULT_REQ_HEADERS}));
|
|
173
179
|
}
|
|
174
180
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
replacement: SANITIZE_REPLACEMENT,
|
|
178
|
-
});
|
|
179
|
-
const extname = path.extname(basename);
|
|
180
|
-
// to determine if we need to unzip the app, we have a number of places
|
|
181
|
-
// to look: content type, content disposition, or the file extension
|
|
182
|
-
if (ZIP_EXTS.has(extname)) {
|
|
183
|
-
fileName = basename;
|
|
184
|
-
shouldUnzipApp = true;
|
|
185
|
-
}
|
|
186
|
-
if (headers['content-type']) {
|
|
187
|
-
const ct = headers['content-type'];
|
|
188
|
-
logger.debug(`Content-Type: ${ct}`);
|
|
189
|
-
// the filetype may not be obvious for certain urls, so check the mime type too
|
|
190
|
-
if (
|
|
191
|
-
ZIP_MIME_TYPES.some((mimeType) =>
|
|
192
|
-
new RegExp(`\\b${_.escapeRegExp(mimeType)}\\b`).test(ct)
|
|
193
|
-
)
|
|
194
|
-
) {
|
|
195
|
-
if (!fileName) {
|
|
196
|
-
fileName = `${DEFAULT_BASENAME}.zip`;
|
|
197
|
-
}
|
|
198
|
-
shouldUnzipApp = true;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
if (headers['content-disposition'] && /^attachment/i.test(headers['content-disposition'])) {
|
|
202
|
-
logger.debug(`Content-Disposition: ${headers['content-disposition']}`);
|
|
203
|
-
const match = /filename="([^"]+)/i.exec(headers['content-disposition']);
|
|
204
|
-
if (match) {
|
|
205
|
-
fileName = fs.sanitizeName(match[1], {
|
|
206
|
-
replacement: SANITIZE_REPLACEMENT,
|
|
207
|
-
});
|
|
208
|
-
shouldUnzipApp = shouldUnzipApp || ZIP_EXTS.has(path.extname(fileName));
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (!fileName) {
|
|
212
|
-
// assign the default file name and the extension if none has been detected
|
|
213
|
-
const resultingName = basename
|
|
214
|
-
? basename.substring(0, basename.length - extname.length)
|
|
215
|
-
: DEFAULT_BASENAME;
|
|
216
|
-
let resultingExt = extname;
|
|
217
|
-
if (!supportedAppExtensions.includes(resultingExt)) {
|
|
218
|
-
logger.info(
|
|
219
|
-
`The current file extension '${resultingExt}' is not supported. ` +
|
|
220
|
-
`Defaulting to '${_.first(supportedAppExtensions)}'`
|
|
221
|
-
);
|
|
222
|
-
resultingExt = /** @type {string} */ (_.first(supportedAppExtensions));
|
|
223
|
-
}
|
|
224
|
-
fileName = `${resultingName}${resultingExt}`;
|
|
225
|
-
}
|
|
226
|
-
newApp = onDownload
|
|
227
|
-
? await onDownload({
|
|
181
|
+
if (onDownload) {
|
|
182
|
+
newApp = await onDownload({
|
|
228
183
|
url: originalAppLink,
|
|
229
184
|
headers: /** @type {import('@appium/types').HTTPHeaders} */ (_.clone(headers)),
|
|
230
185
|
stream,
|
|
231
|
-
})
|
|
232
|
-
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
const fileName = determineFilename(headers, pathname, supportedAppExtensions);
|
|
189
|
+
newApp = await fetchApp(stream, await tempDir.path({
|
|
233
190
|
prefix: fileName,
|
|
234
191
|
suffix: '',
|
|
235
192
|
}));
|
|
193
|
+
}
|
|
236
194
|
} finally {
|
|
237
195
|
if (!stream.closed) {
|
|
238
196
|
stream.destroy();
|
|
@@ -241,7 +199,6 @@ export async function configureApp(
|
|
|
241
199
|
} else if (await fs.exists(newApp)) {
|
|
242
200
|
// Use the local app
|
|
243
201
|
logger.info(`Using local app '${newApp}'`);
|
|
244
|
-
shouldUnzipApp = ZIP_EXTS.has(path.extname(newApp));
|
|
245
202
|
} else {
|
|
246
203
|
let errorMessage = `The application at '${newApp}' does not exist or is not accessible`;
|
|
247
204
|
// protocol value for 'C:\\temp' is 'c:', so we check the length as well
|
|
@@ -258,34 +215,7 @@ export async function configureApp(
|
|
|
258
215
|
packageHash = await calculateFileIntegrity(newApp);
|
|
259
216
|
}
|
|
260
217
|
|
|
261
|
-
|
|
262
|
-
const archivePath = newApp;
|
|
263
|
-
if (packageHash === cachedAppInfo?.packageHash) {
|
|
264
|
-
const fullPath = cachedAppInfo?.fullPath;
|
|
265
|
-
if (await isAppIntegrityOk(/** @type {string} */ (fullPath), cachedAppInfo?.integrity)) {
|
|
266
|
-
if (archivePath !== app) {
|
|
267
|
-
await fs.rimraf(archivePath);
|
|
268
|
-
}
|
|
269
|
-
logger.info(`Will reuse previously cached application at '${fullPath}'`);
|
|
270
|
-
return verifyAppExtension(/** @type {string} */ (fullPath), supportedAppExtensions);
|
|
271
|
-
}
|
|
272
|
-
logger.info(
|
|
273
|
-
`The application at '${fullPath}' does not exist anymore ` +
|
|
274
|
-
`or its integrity has been damaged. Deleting it from the cache`
|
|
275
|
-
);
|
|
276
|
-
APPLICATIONS_CACHE.delete(appCacheKey);
|
|
277
|
-
}
|
|
278
|
-
const tmpRoot = await tempDir.openDir();
|
|
279
|
-
try {
|
|
280
|
-
newApp = await unzipApp(archivePath, tmpRoot, supportedAppExtensions);
|
|
281
|
-
} finally {
|
|
282
|
-
if (newApp !== archivePath && archivePath !== app) {
|
|
283
|
-
await fs.rimraf(archivePath);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
logger.info(`Unzipped local app to '${newApp}'`);
|
|
287
|
-
}
|
|
288
|
-
|
|
218
|
+
/** @type {(appPathToCache: string) => Promise<string>} */
|
|
289
219
|
const storeAppInCache = async (appPathToCache) => {
|
|
290
220
|
const cachedFullPath = cachedAppInfo?.fullPath;
|
|
291
221
|
if (cachedFullPath && cachedFullPath !== appPathToCache) {
|
|
@@ -496,83 +426,6 @@ async function fetchApp(srcStream, dstPath) {
|
|
|
496
426
|
return dstPath;
|
|
497
427
|
}
|
|
498
428
|
|
|
499
|
-
/**
|
|
500
|
-
* Extracts the bundle from an archive into the given folder
|
|
501
|
-
*
|
|
502
|
-
* @param {string} zipPath Full path to the archive containing the bundle
|
|
503
|
-
* @param {string} dstRoot Full path to the folder where the extracted bundle
|
|
504
|
-
* should be placed
|
|
505
|
-
* @param {Array<string>|string} supportedAppExtensions The list of extensions
|
|
506
|
-
* the target application bundle supports, for example ['.apk', '.apks'] for
|
|
507
|
-
* Android packages
|
|
508
|
-
* @returns {Promise<string>} Full path to the bundle in the destination folder
|
|
509
|
-
* @throws {Error} If the given archive is invalid or no application bundles
|
|
510
|
-
* have been found inside
|
|
511
|
-
*/
|
|
512
|
-
async function unzipApp(zipPath, dstRoot, supportedAppExtensions) {
|
|
513
|
-
await zip.assertValidZip(zipPath);
|
|
514
|
-
|
|
515
|
-
if (!_.isArray(supportedAppExtensions)) {
|
|
516
|
-
supportedAppExtensions = [supportedAppExtensions];
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const tmpRoot = await tempDir.openDir();
|
|
520
|
-
try {
|
|
521
|
-
logger.debug(`Unzipping '${zipPath}'`);
|
|
522
|
-
const timer = new timing.Timer().start();
|
|
523
|
-
const useSystemUnzip = isEnvOptionEnabled('APPIUM_PREFER_SYSTEM_UNZIP', true);
|
|
524
|
-
/**
|
|
525
|
-
* Attempt to use use the system `unzip` (e.g., `/usr/bin/unzip`) due
|
|
526
|
-
* to the significant performance improvement it provides over the native
|
|
527
|
-
* JS "unzip" implementation.
|
|
528
|
-
* @type {import('@appium/support/lib/zip').ExtractAllOptions}
|
|
529
|
-
*/
|
|
530
|
-
const extractionOpts = {useSystemUnzip};
|
|
531
|
-
// https://github.com/appium/appium/issues/14100
|
|
532
|
-
if (path.extname(zipPath) === IPA_EXT) {
|
|
533
|
-
logger.debug(
|
|
534
|
-
`Enforcing UTF-8 encoding on the extracted file names for '${path.basename(zipPath)}'`
|
|
535
|
-
);
|
|
536
|
-
extractionOpts.fileNamesEncoding = 'utf8';
|
|
537
|
-
}
|
|
538
|
-
await zip.extractAllTo(zipPath, tmpRoot, extractionOpts);
|
|
539
|
-
const globPattern = `**/*.+(${supportedAppExtensions
|
|
540
|
-
.map((ext) => ext.replace(/^\./, ''))
|
|
541
|
-
.join('|')})`;
|
|
542
|
-
const sortedBundleItems = (
|
|
543
|
-
await fs.glob(globPattern, {
|
|
544
|
-
cwd: tmpRoot,
|
|
545
|
-
// Get the top level match
|
|
546
|
-
})
|
|
547
|
-
).sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
|
|
548
|
-
if (_.isEmpty(sortedBundleItems)) {
|
|
549
|
-
throw logger.errorWithException(
|
|
550
|
-
`App unzipped OK, but we could not find any '${supportedAppExtensions}' ` +
|
|
551
|
-
util.pluralize('bundle', supportedAppExtensions.length, false) +
|
|
552
|
-
` in it. Make sure your archive contains at least one package having ` +
|
|
553
|
-
`'${supportedAppExtensions}' ${util.pluralize(
|
|
554
|
-
'extension',
|
|
555
|
-
supportedAppExtensions.length,
|
|
556
|
-
false
|
|
557
|
-
)}`
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
logger.debug(
|
|
561
|
-
`Extracted ${util.pluralize('bundle item', sortedBundleItems.length, true)} ` +
|
|
562
|
-
`from '${zipPath}' in ${Math.round(
|
|
563
|
-
timer.getDuration().asMilliSeconds
|
|
564
|
-
)}ms: ${sortedBundleItems}`
|
|
565
|
-
);
|
|
566
|
-
const matchedBundle = /** @type {string} */ (_.first(sortedBundleItems));
|
|
567
|
-
logger.info(`Assuming '${matchedBundle}' is the correct bundle`);
|
|
568
|
-
const dstPath = path.resolve(dstRoot, path.basename(matchedBundle));
|
|
569
|
-
await fs.mv(path.resolve(tmpRoot, matchedBundle), dstPath, {mkdirp: true});
|
|
570
|
-
return dstPath;
|
|
571
|
-
} finally {
|
|
572
|
-
await fs.rimraf(tmpRoot);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
429
|
/**
|
|
577
430
|
* Transforms the given app link to the cache key.
|
|
578
431
|
* This is necessary to properly cache apps
|
|
@@ -614,6 +467,47 @@ function parseAppLink(appLink) {
|
|
|
614
467
|
}
|
|
615
468
|
}
|
|
616
469
|
|
|
470
|
+
/**
|
|
471
|
+
* Tries to determine the file name of the payload that is going
|
|
472
|
+
* to be downloaded from an URL
|
|
473
|
+
*
|
|
474
|
+
* @param {import('axios').RawAxiosRequestHeaders} headers
|
|
475
|
+
* @param {string} pathname
|
|
476
|
+
* @param {string[]} supportedAppExtensions
|
|
477
|
+
* @returns {string}
|
|
478
|
+
*/
|
|
479
|
+
function determineFilename(headers, pathname, supportedAppExtensions) {
|
|
480
|
+
const basename = fs.sanitizeName(path.basename(decodeURIComponent(pathname ?? '')), {
|
|
481
|
+
replacement: SANITIZE_REPLACEMENT,
|
|
482
|
+
});
|
|
483
|
+
const extname = path.extname(basename);
|
|
484
|
+
if (headers['content-disposition'] && /^attachment/i.test(
|
|
485
|
+
/** @type {string} */ (headers['content-disposition']
|
|
486
|
+
))) {
|
|
487
|
+
logger.debug(`Content-Disposition: ${headers['content-disposition']}`);
|
|
488
|
+
const match = /filename="([^"]+)/i.exec(/** @type {string} */ (headers['content-disposition']));
|
|
489
|
+
if (match) {
|
|
490
|
+
return fs.sanitizeName(match[1], {
|
|
491
|
+
replacement: SANITIZE_REPLACEMENT,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// assign the default file name and the extension if none has been detected
|
|
497
|
+
const resultingName = basename
|
|
498
|
+
? basename.substring(0, basename.length - extname.length)
|
|
499
|
+
: DEFAULT_BASENAME;
|
|
500
|
+
let resultingExt = extname;
|
|
501
|
+
if (!supportedAppExtensions.map(_.toLower).includes(_.toLower(resultingExt))) {
|
|
502
|
+
logger.info(
|
|
503
|
+
`The current file extension '${resultingExt}' is not supported. ` +
|
|
504
|
+
`Defaulting to '${_.first(supportedAppExtensions)}'`
|
|
505
|
+
);
|
|
506
|
+
resultingExt = /** @type {string} */ (_.first(supportedAppExtensions));
|
|
507
|
+
}
|
|
508
|
+
return `${resultingName}${resultingExt}`;
|
|
509
|
+
}
|
|
510
|
+
|
|
617
511
|
/**
|
|
618
512
|
* Checks whether we can threat the given app link
|
|
619
513
|
* as a URL,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { Constraint } from '@appium/types';
|
|
2
|
+
import log from './logger';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
|
|
5
|
+
export class Validator {
|
|
6
|
+
private readonly _validators: Record<
|
|
7
|
+
keyof Constraint,
|
|
8
|
+
(value: any, options?: any, key?: string) => string | null
|
|
9
|
+
> = {
|
|
10
|
+
isString: (value: any, options?: any): string | null => {
|
|
11
|
+
if (_.isUndefined(value) || _.isNil(options)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (_.isString(value)) {
|
|
16
|
+
return options ? null : 'must not be of type string';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return options ? 'must be of type string' : null;
|
|
20
|
+
},
|
|
21
|
+
isNumber: (value: any, options?: any): string | null => {
|
|
22
|
+
if (_.isUndefined(value) || _.isNil(options)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (_.isNumber(value)) {
|
|
27
|
+
return options ? null : 'must not be of type number';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// allow a string value
|
|
31
|
+
if (options && _.isString(value) && !isNaN(Number(value))) {
|
|
32
|
+
log.warn('Number capability passed in as string. Functionality may be compromised.');
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return options ? 'must be of type number' : null;
|
|
37
|
+
},
|
|
38
|
+
isBoolean: (value: any, options?: any): string | null => {
|
|
39
|
+
if (_.isUndefined(value) || _.isNil(options)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (_.isBoolean(value)) {
|
|
44
|
+
return options ? null : 'must not be of type boolean';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// allow a string value
|
|
48
|
+
if (options && _.isString(value) && ['true', 'false', ''].includes(value)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return options ? 'must be of type boolean' : null;
|
|
53
|
+
},
|
|
54
|
+
isObject: (value: any, options?: any): string | null => {
|
|
55
|
+
if (_.isUndefined(value) || _.isNil(options)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (_.isPlainObject(value)) {
|
|
60
|
+
return options ? null : 'must not be a plain object';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return options ? 'must be a plain object' : null;
|
|
64
|
+
},
|
|
65
|
+
isArray: (value: any, options?: any): string | null => {
|
|
66
|
+
if (_.isUndefined(value) || _.isNil(options)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (_.isArray(value)) {
|
|
71
|
+
return options ? null : 'must not be of type array';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return options ? 'must be of type array' : null;
|
|
75
|
+
},
|
|
76
|
+
deprecated: (value: any, options?: any, key?: string): string | null => {
|
|
77
|
+
if (!_.isUndefined(value) && options) {
|
|
78
|
+
log.warn(
|
|
79
|
+
`The '${key}' capability has been deprecated and must not be used anymore. ` +
|
|
80
|
+
`Please check the driver documentation for possible alternatives.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
},
|
|
85
|
+
inclusion: (value: any, options?: any): string | null => {
|
|
86
|
+
if (_.isUndefined(value) || !options) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const optionsArr = _.isArray(options) ? options : [options];
|
|
90
|
+
if (optionsArr.some((opt) => opt === value)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return `must be contained by ${JSON.stringify(optionsArr)}`;
|
|
94
|
+
},
|
|
95
|
+
inclusionCaseInsensitive: (value: any, options?: any): string | null => {
|
|
96
|
+
if (_.isUndefined(value) || !options) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const optionsArr = _.isArray(options) ? options : [options];
|
|
100
|
+
if (optionsArr.some((opt) => _.toLower(opt) === _.toLower(value))) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return `must be contained by ${JSON.stringify(optionsArr)}`;
|
|
104
|
+
},
|
|
105
|
+
presence: (value: any, options?: any): string | null => {
|
|
106
|
+
if (_.isUndefined(value) && options) {
|
|
107
|
+
return 'is required to be present';
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
!options?.allowEmpty &&
|
|
111
|
+
((!_.isUndefined(value) && _.isEmpty(value)) || (_.isString(value) && !_.trim(value)))
|
|
112
|
+
) {
|
|
113
|
+
return 'must not be empty or blank';
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
validate(values: Record<string, any>, constraints: Record<string, Constraint>): Record<string, string[]> | null {
|
|
120
|
+
const result: Record<string, string[]> = {};
|
|
121
|
+
for (const [key, constraint] of _.toPairs(constraints)) {
|
|
122
|
+
const value = values[key];
|
|
123
|
+
for (const [validatorName, options] of _.toPairs(constraint)) {
|
|
124
|
+
if (!(validatorName in this._validators)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const validationError = this._validators[validatorName](value, options, key);
|
|
129
|
+
if (_.isNil(validationError)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (key in result) {
|
|
134
|
+
result[key].push(validationError);
|
|
135
|
+
} else {
|
|
136
|
+
result[key] = [validationError];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return _.isEmpty(result) ? null : result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const validator = new Validator();
|
|
@@ -5,6 +5,9 @@ export {handleIdempotency} from './idempotency';
|
|
|
5
5
|
import {match} from 'path-to-regexp';
|
|
6
6
|
import {util} from '@appium/support';
|
|
7
7
|
import {calcSignature} from '../helpers/session';
|
|
8
|
+
import {getResponseForW3CError} from '../protocol/errors';
|
|
9
|
+
|
|
10
|
+
const SESSION_ID_PATTERN = /\/session\/([^/]+)/;
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
*
|
|
@@ -14,20 +17,16 @@ import {calcSignature} from '../helpers/session';
|
|
|
14
17
|
* @returns {any}
|
|
15
18
|
*/
|
|
16
19
|
export function allowCrossDomain(req, res, next) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
);
|
|
20
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
21
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS, DELETE');
|
|
22
|
+
res.header(
|
|
23
|
+
'Access-Control-Allow-Headers',
|
|
24
|
+
'Cache-Control, Pragma, Origin, X-Requested-With, Content-Type, Accept, User-Agent'
|
|
25
|
+
);
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
} catch (err) {
|
|
30
|
-
log.error(`Unexpected error: ${err.stack}`);
|
|
27
|
+
// need to respond 200 to OPTIONS
|
|
28
|
+
if ('OPTIONS' === req.method) {
|
|
29
|
+
return res.sendStatus(200);
|
|
31
30
|
}
|
|
32
31
|
next();
|
|
33
32
|
}
|
|
@@ -50,26 +49,6 @@ export function allowCrossDomainAsyncExecute(basePath) {
|
|
|
50
49
|
};
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
/**
|
|
54
|
-
*
|
|
55
|
-
* @param {string} basePath
|
|
56
|
-
* @returns {import('express').RequestHandler}
|
|
57
|
-
*/
|
|
58
|
-
export function fixPythonContentType(basePath) {
|
|
59
|
-
return (req, res, next) => {
|
|
60
|
-
// hack because python client library gives us wrong content-type
|
|
61
|
-
if (
|
|
62
|
-
new RegExp(`^${_.escapeRegExp(basePath)}`).test(req.path) &&
|
|
63
|
-
/^Python/.test(req.headers['user-agent'] ?? '')
|
|
64
|
-
) {
|
|
65
|
-
if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
|
|
66
|
-
req.headers['content-type'] = 'application/json; charset=utf-8';
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
next();
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
52
|
/**
|
|
74
53
|
*
|
|
75
54
|
* @param {import('express').Request} req
|
|
@@ -78,12 +57,17 @@ export function fixPythonContentType(basePath) {
|
|
|
78
57
|
* @returns {any}
|
|
79
58
|
*/
|
|
80
59
|
export function handleLogContext(req, res, next) {
|
|
81
|
-
const requestId = util.uuidV4();
|
|
60
|
+
const requestId = fetchHeaderValue(req, 'x-request-id') || util.uuidV4();
|
|
82
61
|
|
|
83
62
|
const sessionId = SESSION_ID_PATTERN.exec(req.url)?.[1];
|
|
84
63
|
const sessionInfo = sessionId ? {sessionId, sessionSignature: calcSignature(sessionId)} : {};
|
|
64
|
+
const isSensitiveHeaderValue = fetchHeaderValue(req, 'x-appium-is-sensitive');
|
|
85
65
|
|
|
86
|
-
log.updateAsyncContext({
|
|
66
|
+
log.updateAsyncContext({
|
|
67
|
+
requestId,
|
|
68
|
+
...sessionInfo,
|
|
69
|
+
isSensitive: ['true', '1', 'yes'].includes(_.toLower(isSensitiveHeaderValue)),
|
|
70
|
+
}, true);
|
|
87
71
|
|
|
88
72
|
return next();
|
|
89
73
|
}
|
|
@@ -142,19 +126,8 @@ export function catchAllHandler(err, req, res, next) {
|
|
|
142
126
|
}
|
|
143
127
|
|
|
144
128
|
log.error(`Uncaught error: ${err.message}`);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
res.status(error.w3cStatus()).json(
|
|
148
|
-
patchWithSessionId(req, {
|
|
149
|
-
status: error.code(),
|
|
150
|
-
value: {
|
|
151
|
-
error: error.error(),
|
|
152
|
-
message: `An unknown server-side error occurred while processing the command: ${err.message}`,
|
|
153
|
-
stacktrace: err.stack,
|
|
154
|
-
},
|
|
155
|
-
})
|
|
156
|
-
);
|
|
157
|
-
log.error(err);
|
|
129
|
+
const [status, body] = getResponseForW3CError(err);
|
|
130
|
+
res.status(status).json(body);
|
|
158
131
|
}
|
|
159
132
|
|
|
160
133
|
/**
|
|
@@ -163,28 +136,17 @@ export function catchAllHandler(err, req, res, next) {
|
|
|
163
136
|
*/
|
|
164
137
|
export function catch404Handler(req, res) {
|
|
165
138
|
log.debug(`No route found for ${req.url}`);
|
|
166
|
-
const
|
|
167
|
-
res.status(
|
|
168
|
-
patchWithSessionId(req, {
|
|
169
|
-
status: error.code(),
|
|
170
|
-
value: {
|
|
171
|
-
error: error.error(),
|
|
172
|
-
message:
|
|
173
|
-
'The requested resource could not be found, or a request was ' +
|
|
174
|
-
'received using an HTTP method that is not supported by the mapped ' +
|
|
175
|
-
'resource',
|
|
176
|
-
stacktrace: '',
|
|
177
|
-
},
|
|
178
|
-
})
|
|
179
|
-
);
|
|
139
|
+
const [status, body] = getResponseForW3CError(new errors.UnknownCommandError());
|
|
140
|
+
res.status(status).json(body);
|
|
180
141
|
}
|
|
181
142
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
143
|
+
/**
|
|
144
|
+
* @param {import('express').Request} req
|
|
145
|
+
* @param {string} name
|
|
146
|
+
* @returns {string | undefined}
|
|
147
|
+
*/
|
|
148
|
+
function fetchHeaderValue(req, name) {
|
|
149
|
+
return _.isArray(req.headers[name])
|
|
150
|
+
? req.headers[name][0]
|
|
151
|
+
: req.headers[name];
|
|
190
152
|
}
|
package/lib/express/server.js
CHANGED
|
@@ -9,7 +9,6 @@ import log from './logger';
|
|
|
9
9
|
import {startLogFormatter, endLogFormatter} from './express-logging';
|
|
10
10
|
import {
|
|
11
11
|
allowCrossDomain,
|
|
12
|
-
fixPythonContentType,
|
|
13
12
|
defaultToJSONContentType,
|
|
14
13
|
catchAllHandler,
|
|
15
14
|
allowCrossDomainAsyncExecute,
|
|
@@ -172,7 +171,6 @@ export function configureServer({
|
|
|
172
171
|
app.use(allowCrossDomainAsyncExecute(basePath));
|
|
173
172
|
}
|
|
174
173
|
app.use(handleIdempotency);
|
|
175
|
-
app.use(fixPythonContentType(basePath));
|
|
176
174
|
app.use(defaultToJSONContentType);
|
|
177
175
|
app.use(bodyParser.urlencoded({extended: true}));
|
|
178
176
|
app.use(methodOverride());
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import _ from 'lodash';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Determine whether the given agument is valid
|
|
7
|
+
* W3C capabilities instance.
|
|
8
|
+
*
|
|
9
|
+
* @param {any} caps
|
|
10
|
+
* @returns {caps is import('@appium/types').W3CCapabilities}
|
|
11
|
+
*/
|
|
12
|
+
export function isW3cCaps(caps) {
|
|
6
13
|
if (!_.isPlainObject(caps)) {
|
|
7
14
|
return false;
|
|
8
15
|
}
|
|
@@ -32,7 +39,7 @@ function isW3cCaps(caps) {
|
|
|
32
39
|
* @param {AppiumLogger} log
|
|
33
40
|
* @returns {Capabilities<C>}
|
|
34
41
|
*/
|
|
35
|
-
function fixCaps(oldCaps, desiredCapConstraints, log) {
|
|
42
|
+
export function fixCaps(oldCaps, desiredCapConstraints, log) {
|
|
36
43
|
let caps = _.clone(oldCaps);
|
|
37
44
|
|
|
38
45
|
// boolean capabilities can be passed in as strings 'false' and 'true'
|
|
@@ -73,8 +80,6 @@ function fixCaps(oldCaps, desiredCapConstraints, log) {
|
|
|
73
80
|
return caps;
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
export {isW3cCaps, fixCaps};
|
|
77
|
-
|
|
78
83
|
/**
|
|
79
84
|
* @typedef {import('@appium/types').Constraints} Constraints
|
|
80
85
|
* @typedef {import('@appium/types').AppiumLogger} AppiumLogger
|
package/lib/index.js
CHANGED
|
@@ -54,6 +54,8 @@ export {BIDI_COMMANDS} from './protocol/bidi-commands';
|
|
|
54
54
|
|
|
55
55
|
export {generateDriverLogPrefix} from './basedriver/helpers';
|
|
56
56
|
|
|
57
|
+
export {isW3cCaps} from './helpers/capabilities';
|
|
58
|
+
|
|
57
59
|
/**
|
|
58
60
|
* @typedef {import('./express/server').ServerOpts} ServerOpts
|
|
59
61
|
*/
|