@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.
Files changed (94) hide show
  1. package/README.md +0 -8
  2. package/build/lib/basedriver/capabilities.d.ts.map +1 -1
  3. package/build/lib/basedriver/capabilities.js +2 -4
  4. package/build/lib/basedriver/capabilities.js.map +1 -1
  5. package/build/lib/basedriver/commands/execute.js.map +1 -1
  6. package/build/lib/basedriver/commands/timeout.js +12 -31
  7. package/build/lib/basedriver/commands/timeout.js.map +1 -1
  8. package/build/lib/basedriver/core.d.ts +0 -8
  9. package/build/lib/basedriver/core.d.ts.map +1 -1
  10. package/build/lib/basedriver/core.js +8 -18
  11. package/build/lib/basedriver/core.js.map +1 -1
  12. package/build/lib/basedriver/driver.js +2 -2
  13. package/build/lib/basedriver/driver.js.map +1 -1
  14. package/build/lib/basedriver/helpers.d.ts +9 -1
  15. package/build/lib/basedriver/helpers.d.ts.map +1 -1
  16. package/build/lib/basedriver/helpers.js +56 -142
  17. package/build/lib/basedriver/helpers.js.map +1 -1
  18. package/build/lib/basedriver/validation.d.ts +7 -0
  19. package/build/lib/basedriver/validation.d.ts.map +1 -0
  20. package/build/lib/basedriver/validation.js +130 -0
  21. package/build/lib/basedriver/validation.js.map +1 -0
  22. package/build/lib/express/middleware.d.ts +0 -6
  23. package/build/lib/express/middleware.d.ts.map +1 -1
  24. package/build/lib/express/middleware.js +28 -60
  25. package/build/lib/express/middleware.js.map +1 -1
  26. package/build/lib/express/server.d.ts.map +1 -1
  27. package/build/lib/express/server.js +0 -1
  28. package/build/lib/express/server.js.map +1 -1
  29. package/build/lib/helpers/capabilities.d.ts +13 -6
  30. package/build/lib/helpers/capabilities.d.ts.map +1 -1
  31. package/build/lib/helpers/capabilities.js +7 -0
  32. package/build/lib/helpers/capabilities.js.map +1 -1
  33. package/build/lib/index.d.ts +1 -0
  34. package/build/lib/index.d.ts.map +1 -1
  35. package/build/lib/index.js +3 -1
  36. package/build/lib/index.js.map +1 -1
  37. package/build/lib/jsonwp-proxy/proxy.d.ts +0 -8
  38. package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
  39. package/build/lib/jsonwp-proxy/proxy.js +7 -38
  40. package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
  41. package/build/lib/protocol/errors.d.ts +171 -277
  42. package/build/lib/protocol/errors.d.ts.map +1 -1
  43. package/build/lib/protocol/errors.js +201 -421
  44. package/build/lib/protocol/errors.js.map +1 -1
  45. package/build/lib/protocol/helpers.d.ts +6 -6
  46. package/build/lib/protocol/helpers.d.ts.map +1 -1
  47. package/build/lib/protocol/helpers.js +11 -7
  48. package/build/lib/protocol/helpers.js.map +1 -1
  49. package/build/lib/protocol/index.d.ts +2 -1
  50. package/build/lib/protocol/index.d.ts.map +1 -1
  51. package/build/lib/protocol/index.js +2 -1
  52. package/build/lib/protocol/index.js.map +1 -1
  53. package/build/lib/protocol/protocol.d.ts +16 -19
  54. package/build/lib/protocol/protocol.d.ts.map +1 -1
  55. package/build/lib/protocol/protocol.js +98 -119
  56. package/build/lib/protocol/protocol.js.map +1 -1
  57. package/build/lib/protocol/routes.d.ts +12 -714
  58. package/build/lib/protocol/routes.d.ts.map +1 -1
  59. package/build/lib/protocol/routes.js +24 -488
  60. package/build/lib/protocol/routes.js.map +1 -1
  61. package/build/lib/protocol/validators.d.ts +4 -7
  62. package/build/lib/protocol/validators.d.ts.map +1 -1
  63. package/build/lib/protocol/validators.js +4 -21
  64. package/build/lib/protocol/validators.js.map +1 -1
  65. package/lib/basedriver/capabilities.ts +2 -4
  66. package/lib/basedriver/commands/execute.ts +1 -1
  67. package/lib/basedriver/commands/timeout.ts +16 -43
  68. package/lib/basedriver/core.ts +10 -19
  69. package/lib/basedriver/driver.ts +3 -3
  70. package/lib/basedriver/helpers.js +61 -167
  71. package/lib/basedriver/validation.ts +145 -0
  72. package/lib/express/middleware.js +32 -70
  73. package/lib/express/server.js +0 -2
  74. package/lib/helpers/capabilities.js +9 -4
  75. package/lib/index.js +2 -0
  76. package/lib/jsonwp-proxy/proxy.js +8 -45
  77. package/lib/protocol/{errors.js → errors.ts} +322 -436
  78. package/lib/protocol/helpers.js +12 -8
  79. package/lib/protocol/index.js +8 -1
  80. package/lib/protocol/{protocol.js → protocol.ts} +147 -146
  81. package/lib/protocol/routes.js +26 -498
  82. package/lib/protocol/validators.ts +19 -0
  83. package/package.json +10 -11
  84. package/build/lib/basedriver/desired-caps.d.ts +0 -5
  85. package/build/lib/basedriver/desired-caps.d.ts.map +0 -1
  86. package/build/lib/basedriver/desired-caps.js +0 -92
  87. package/build/lib/basedriver/desired-caps.js.map +0 -1
  88. package/lib/basedriver/README.md +0 -36
  89. package/lib/basedriver/desired-caps.js +0 -103
  90. package/lib/express/README.md +0 -59
  91. package/lib/jsonwp-proxy/README.md +0 -52
  92. package/lib/jsonwp-status/README.md +0 -20
  93. package/lib/protocol/README.md +0 -100
  94. 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, zip, timing, node} from '@appium/support';
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
- let fileName = null;
176
- const basename = fs.sanitizeName(path.basename(decodeURIComponent(pathname ?? '')), {
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
- : await fetchApp(stream, await tempDir.path({
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
- if (isPackageAFile && shouldUnzipApp && !_.isFunction(onPostProcess)) {
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
- try {
18
- res.header('Access-Control-Allow-Origin', '*');
19
- res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS, DELETE');
20
- res.header(
21
- 'Access-Control-Allow-Headers',
22
- 'Cache-Control, Pragma, Origin, X-Requested-With, Content-Type, Accept, User-Agent'
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
- // need to respond 200 to OPTIONS
26
- if ('OPTIONS' === req.method) {
27
- return res.sendStatus(200);
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({requestId, ...sessionInfo}, true);
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
- log.error('Sending generic error response');
146
- const error = errors.UnknownError;
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 error = errors.UnknownCommandError;
167
- res.status(error.w3cStatus()).json(
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
- const SESSION_ID_PATTERN = /\/session\/([^/]+)/;
183
-
184
- function patchWithSessionId(req, body) {
185
- const match = SESSION_ID_PATTERN.exec(req.url);
186
- if (match) {
187
- body.sessionId = match[1];
188
- }
189
- return body;
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
  }
@@ -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
- function isW3cCaps(caps) {
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
  */