@appium/base-driver 10.2.0 → 10.2.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 (138) hide show
  1. package/LICENSE +201 -0
  2. package/build/lib/basedriver/capabilities.js +7 -7
  3. package/build/lib/basedriver/capabilities.js.map +1 -1
  4. package/build/lib/basedriver/commands/event.d.ts +1 -1
  5. package/build/lib/basedriver/commands/event.d.ts.map +1 -1
  6. package/build/lib/basedriver/commands/execute.d.ts +1 -1
  7. package/build/lib/basedriver/commands/execute.d.ts.map +1 -1
  8. package/build/lib/basedriver/commands/find.d.ts +1 -1
  9. package/build/lib/basedriver/commands/find.d.ts.map +1 -1
  10. package/build/lib/basedriver/commands/mixin.d.ts +1 -1
  11. package/build/lib/basedriver/commands/mixin.d.ts.map +1 -1
  12. package/build/lib/basedriver/commands/timeout.d.ts +1 -1
  13. package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
  14. package/build/lib/basedriver/device-settings.d.ts +14 -23
  15. package/build/lib/basedriver/device-settings.d.ts.map +1 -1
  16. package/build/lib/basedriver/device-settings.js +11 -26
  17. package/build/lib/basedriver/device-settings.js.map +1 -1
  18. package/build/lib/basedriver/helpers.d.ts +36 -57
  19. package/build/lib/basedriver/helpers.d.ts.map +1 -1
  20. package/build/lib/basedriver/helpers.js +148 -239
  21. package/build/lib/basedriver/helpers.js.map +1 -1
  22. package/build/lib/basedriver/logger.d.ts +1 -2
  23. package/build/lib/basedriver/logger.d.ts.map +1 -1
  24. package/build/lib/basedriver/logger.js +2 -2
  25. package/build/lib/basedriver/logger.js.map +1 -1
  26. package/build/lib/basedriver/validation.d.ts.map +1 -1
  27. package/build/lib/basedriver/validation.js +3 -3
  28. package/build/lib/basedriver/validation.js.map +1 -1
  29. package/build/lib/constants.d.ts +1 -1
  30. package/build/lib/constants.d.ts.map +1 -1
  31. package/build/lib/express/crash.d.ts +8 -2
  32. package/build/lib/express/crash.d.ts.map +1 -1
  33. package/build/lib/express/crash.js +6 -0
  34. package/build/lib/express/crash.js.map +1 -1
  35. package/build/lib/express/express-logging.d.ts +12 -2
  36. package/build/lib/express/express-logging.d.ts.map +1 -1
  37. package/build/lib/express/express-logging.js +34 -26
  38. package/build/lib/express/express-logging.js.map +1 -1
  39. package/build/lib/express/idempotency.d.ts +4 -10
  40. package/build/lib/express/idempotency.d.ts.map +1 -1
  41. package/build/lib/express/idempotency.js +69 -73
  42. package/build/lib/express/idempotency.js.map +1 -1
  43. package/build/lib/express/logger.d.ts +1 -2
  44. package/build/lib/express/logger.d.ts.map +1 -1
  45. package/build/lib/express/logger.js +2 -2
  46. package/build/lib/express/logger.js.map +1 -1
  47. package/build/lib/express/middleware.d.ts +37 -41
  48. package/build/lib/express/middleware.d.ts.map +1 -1
  49. package/build/lib/express/middleware.js +48 -60
  50. package/build/lib/express/middleware.js.map +1 -1
  51. package/build/lib/express/server.d.ts +57 -101
  52. package/build/lib/express/server.d.ts.map +1 -1
  53. package/build/lib/express/server.js +51 -128
  54. package/build/lib/express/server.js.map +1 -1
  55. package/build/lib/express/static.d.ts +10 -5
  56. package/build/lib/express/static.d.ts.map +1 -1
  57. package/build/lib/express/static.js +32 -42
  58. package/build/lib/express/static.js.map +1 -1
  59. package/build/lib/express/websocket.d.ts +22 -6
  60. package/build/lib/express/websocket.d.ts.map +1 -1
  61. package/build/lib/express/websocket.js +10 -15
  62. package/build/lib/express/websocket.js.map +1 -1
  63. package/build/lib/helpers/capabilities.d.ts +4 -16
  64. package/build/lib/helpers/capabilities.d.ts.map +1 -1
  65. package/build/lib/helpers/capabilities.js +36 -48
  66. package/build/lib/helpers/capabilities.js.map +1 -1
  67. package/build/lib/jsonwp-proxy/protocol-converter.d.ts +42 -78
  68. package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
  69. package/build/lib/jsonwp-proxy/protocol-converter.js +87 -139
  70. package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
  71. package/build/lib/jsonwp-proxy/proxy.d.ts +1 -1
  72. package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
  73. package/build/lib/jsonwp-proxy/proxy.js +2 -2
  74. package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
  75. package/build/lib/jsonwp-status/status.d.ts +113 -158
  76. package/build/lib/jsonwp-status/status.d.ts.map +1 -1
  77. package/build/lib/jsonwp-status/status.js +10 -14
  78. package/build/lib/jsonwp-status/status.js.map +1 -1
  79. package/build/lib/protocol/bidi-commands.d.ts +31 -36
  80. package/build/lib/protocol/bidi-commands.d.ts.map +1 -1
  81. package/build/lib/protocol/bidi-commands.js +5 -5
  82. package/build/lib/protocol/bidi-commands.js.map +1 -1
  83. package/build/lib/protocol/errors.d.ts.map +1 -1
  84. package/build/lib/protocol/helpers.d.ts +7 -11
  85. package/build/lib/protocol/helpers.d.ts.map +1 -1
  86. package/build/lib/protocol/helpers.js +5 -9
  87. package/build/lib/protocol/helpers.js.map +1 -1
  88. package/build/lib/protocol/index.d.ts +4 -21
  89. package/build/lib/protocol/index.d.ts.map +1 -1
  90. package/build/lib/protocol/index.js.map +1 -1
  91. package/build/lib/protocol/protocol.d.ts +15 -1
  92. package/build/lib/protocol/protocol.d.ts.map +1 -1
  93. package/build/lib/protocol/protocol.js +50 -20
  94. package/build/lib/protocol/protocol.js.map +1 -1
  95. package/build/lib/protocol/routes.d.ts +8 -15
  96. package/build/lib/protocol/routes.d.ts.map +1 -1
  97. package/build/lib/protocol/routes.js +18 -33
  98. package/build/lib/protocol/routes.js.map +1 -1
  99. package/lib/basedriver/capabilities.ts +1 -1
  100. package/lib/basedriver/commands/event.ts +2 -2
  101. package/lib/basedriver/commands/execute.ts +2 -2
  102. package/lib/basedriver/commands/find.ts +2 -2
  103. package/lib/basedriver/commands/mixin.ts +1 -1
  104. package/lib/basedriver/commands/timeout.ts +2 -2
  105. package/lib/basedriver/{device-settings.js → device-settings.ts} +24 -35
  106. package/lib/basedriver/{helpers.js → helpers.ts} +208 -266
  107. package/lib/basedriver/logger.ts +3 -0
  108. package/lib/basedriver/validation.ts +2 -2
  109. package/lib/constants.ts +1 -1
  110. package/lib/express/crash.ts +15 -0
  111. package/lib/express/express-logging.ts +84 -0
  112. package/lib/express/{idempotency.js → idempotency.ts} +105 -89
  113. package/lib/express/logger.ts +3 -0
  114. package/lib/express/middleware.ts +187 -0
  115. package/lib/express/{server.js → server.ts} +175 -167
  116. package/lib/express/static.ts +77 -0
  117. package/lib/express/websocket.ts +81 -0
  118. package/lib/helpers/capabilities.ts +83 -0
  119. package/lib/jsonwp-proxy/protocol-converter.ts +284 -0
  120. package/lib/jsonwp-proxy/proxy.js +1 -1
  121. package/lib/jsonwp-status/{status.js → status.ts} +12 -15
  122. package/lib/protocol/{bidi-commands.js → bidi-commands.ts} +7 -5
  123. package/lib/protocol/errors.ts +1 -1
  124. package/lib/protocol/{helpers.js → helpers.ts} +8 -11
  125. package/lib/protocol/protocol.ts +57 -26
  126. package/lib/protocol/{routes.js → routes.ts} +29 -40
  127. package/package.json +11 -11
  128. package/tsconfig.json +3 -1
  129. package/lib/basedriver/logger.js +0 -4
  130. package/lib/express/crash.js +0 -11
  131. package/lib/express/express-logging.js +0 -60
  132. package/lib/express/logger.js +0 -4
  133. package/lib/express/middleware.js +0 -171
  134. package/lib/express/static.js +0 -76
  135. package/lib/express/websocket.js +0 -79
  136. package/lib/helpers/capabilities.js +0 -93
  137. package/lib/jsonwp-proxy/protocol-converter.js +0 -317
  138. /package/lib/protocol/{index.js → index.ts} +0 -0
@@ -0,0 +1,3 @@
1
+ import {logger} from '@appium/support';
2
+
3
+ export const log = logger.getLogger('BaseDriver');
@@ -1,5 +1,5 @@
1
- import type { Constraint } from '@appium/types';
2
- import log from './logger';
1
+ import type {Constraint} from '@appium/types';
2
+ import {log} from './logger';
3
3
  import _ from 'lodash';
4
4
 
5
5
  export class Validator {
package/lib/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import {util} from '@appium/support';
2
- import {Protocol} from '@appium/types';
2
+ import type {Protocol} from '@appium/types';
3
3
 
4
4
  // The default maximum length of a single log record
5
5
  // containing http request/response body
@@ -0,0 +1,15 @@
1
+ import {errors} from '../protocol';
2
+
3
+ /**
4
+ * Route handler that throws {@link errors.UnknownCommandError} for testing.
5
+ */
6
+ export function produceError(): never {
7
+ throw new errors.UnknownCommandError('Produced generic error for testing');
8
+ }
9
+
10
+ /**
11
+ * Route handler that throws a generic Error for testing (e.g. crash scenarios).
12
+ */
13
+ export function produceCrash(): never {
14
+ throw new Error('We just tried to crash Appium!');
15
+ }
@@ -0,0 +1,84 @@
1
+ import _ from 'lodash';
2
+ import '@colors/colors';
3
+ import morgan from 'morgan';
4
+ import type {Request, RequestHandler, Response} from 'express';
5
+ import {log} from './logger';
6
+ import {MAX_LOG_BODY_LENGTH} from '../constants';
7
+ import {logger} from '@appium/support';
8
+
9
+ /**
10
+ * Morgan middleware that logs when the HTTP response finishes.
11
+ * Logs method, URL, status (color-coded), response time, and content-length.
12
+ */
13
+ export const endLogFormatter: RequestHandler = morgan(endLogFormatterHandler);
14
+
15
+ /**
16
+ * Morgan middleware that logs when the HTTP request is received (immediate).
17
+ * Logs method and URL; request body is truncated and passed through {@link logger.markSensitive}.
18
+ */
19
+ export const startLogFormatter: RequestHandler = morgan(startLogFormatterHandler, {
20
+ immediate: true,
21
+ });
22
+
23
+ // #region Private types and helpers
24
+ type MorganTokens = unknown;
25
+ type FormatFn = (tokens: MorganTokens, req: Request, res: Response) => string;
26
+
27
+ function endLogFormatterHandler(tokens: MorganTokens, req: Request, res: Response): void {
28
+ log.info(requestEndLoggingFormat(tokens, req, res));
29
+ }
30
+
31
+ function startLogFormatterHandler(tokens: unknown, req: Request, res: Response): void {
32
+ let reqBody = '';
33
+ if (req.body) {
34
+ try {
35
+ reqBody = _.truncate(
36
+ _.isString(req.body) ? req.body : JSON.stringify(req.body),
37
+ {length: MAX_LOG_BODY_LENGTH}
38
+ );
39
+ } catch {
40
+ // ignore
41
+ }
42
+ }
43
+ log.info(
44
+ requestStartLoggingFormat(tokens, req, res),
45
+ logger.markSensitive(reqBody.grey)
46
+ );
47
+ }
48
+
49
+ // Copied the morgan compile function over so that cooler formats may be configured
50
+ function compile(fmt: string): FormatFn {
51
+ fmt = fmt.replace(/"/g, '\\"');
52
+ fmt = fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g, function replace(_, name, arg) {
53
+ return `"\n + (tokens["${name}"](req, res, "${arg}") || "-") + "`;
54
+ });
55
+ const js = ` return "${fmt}";`;
56
+ return new Function('tokens', 'req', 'res', js) as FormatFn;
57
+ }
58
+
59
+ function requestEndLoggingFormat(
60
+ tokens: MorganTokens,
61
+ req: Request,
62
+ res: Response
63
+ ): string {
64
+ const status = res.statusCode;
65
+ let statusStr = ':status';
66
+ if (status >= 500) {
67
+ statusStr = statusStr.red;
68
+ } else if (status >= 400) {
69
+ statusStr = statusStr.yellow;
70
+ } else if (status >= 300) {
71
+ statusStr = statusStr.cyan;
72
+ } else {
73
+ statusStr = statusStr.green;
74
+ }
75
+ const fn = compile(
76
+ `${'<-- :method :url '.white}${statusStr} ${':response-time ms - :res[content-length]'.grey}`
77
+ );
78
+ return fn(tokens, req, res);
79
+ }
80
+
81
+ const requestStartLoggingFormat = compile(
82
+ `${'-->'.white} ${':method'.white} ${':url'.white}`
83
+ );
84
+ // #endregion
@@ -1,37 +1,99 @@
1
- import log from './logger';
2
- import { LRUCache } from 'lru-cache';
1
+ import {log} from './logger';
2
+ import {LRUCache} from 'lru-cache';
3
3
  import _ from 'lodash';
4
4
  import {EventEmitter} from 'node:events';
5
+ import type {Request, Response, NextFunction} from 'express';
5
6
 
6
- const IDEMPOTENT_RESPONSES = new LRUCache({
7
+ interface CachedResponse {
8
+ method: string;
9
+ path: string;
10
+ response: Buffer | null;
11
+ responseStateListener: EventEmitter | null | undefined;
12
+ }
13
+
14
+ const IDEMPOTENT_RESPONSES = new LRUCache<string, CachedResponse>({
7
15
  max: 64,
8
16
  ttl: 30 * 60 * 1000,
9
17
  updateAgeOnGet: true,
10
18
  updateAgeOnHas: true,
11
- // @ts-ignore The value must contain responseStateListener
12
19
  dispose: ({responseStateListener}) => {
13
20
  responseStateListener?.removeAllListeners();
14
- }
21
+ },
15
22
  });
16
23
  const MONITORED_METHODS = ['POST', 'PATCH'];
17
24
  const IDEMPOTENCY_KEY_HEADER = 'x-idempotency-key';
18
25
  const MAX_CACHED_PAYLOAD_SIZE_BYTES = 1 * 1024 * 1024; // 1 MiB
19
26
 
20
27
  /**
21
- * @typedef {Object} CachedResponse
22
- * @property {string} method
23
- * @property {string} path
24
- * @property {Buffer?} response
25
- * @property {EventEmitter|null|undefined} responseStateListener
28
+ * Middleware that caches and replays responses for idempotent requests using the
29
+ * `x-idempotency-key` header. Only POST and PATCH are cached.
26
30
  */
31
+ export async function handleIdempotency(
32
+ req: Request,
33
+ res: Response,
34
+ next: NextFunction
35
+ ): Promise<void> {
36
+ const keyOrArr = req.headers[IDEMPOTENCY_KEY_HEADER];
37
+ if (_.isEmpty(keyOrArr) || !keyOrArr) {
38
+ next();
39
+ return;
40
+ }
27
41
 
28
- /**
29
- *
30
- * @param {string} key
31
- * @param {import('express').Request} req
32
- * @param {import('express').Response} res
33
- */
34
- function cacheResponse(key, req, res) {
42
+ const key = _.isArray(keyOrArr) ? keyOrArr[0] : keyOrArr;
43
+
44
+ log.updateAsyncContext({idempotencyKey: key});
45
+
46
+ if (!MONITORED_METHODS.includes(req.method)) {
47
+ next();
48
+ return;
49
+ }
50
+
51
+ log.debug(`Request idempotency key: ${key}`);
52
+ if (!IDEMPOTENT_RESPONSES.has(key)) {
53
+ cacheResponse(key, req, res);
54
+ next();
55
+ return;
56
+ }
57
+
58
+ const cached = IDEMPOTENT_RESPONSES.get(key);
59
+ if (!cached) {
60
+ next();
61
+ return;
62
+ }
63
+ const {method, path, response, responseStateListener} = cached;
64
+ if (req.method !== method || req.path !== path) {
65
+ log.warn(`Got two different requests with the same idempotency key '${key}'`);
66
+ log.warn('Is the client generating idempotency keys properly?');
67
+ next();
68
+ return;
69
+ }
70
+
71
+ if (response) {
72
+ log.info(`The same request with the idempotency key '${key}' has been already processed`);
73
+ log.info(`Rerouting its response to the current request`);
74
+ if (!res.socket?.writable) {
75
+ next();
76
+ return;
77
+ }
78
+ res.socket.write(response.toString('utf8'));
79
+ } else {
80
+ log.info(`The same request with the idempotency key '${key}' is being processed`);
81
+ log.info(`Waiting for the response to be rerouted to the current request`);
82
+ if (!responseStateListener) {
83
+ next();
84
+ return;
85
+ }
86
+ responseStateListener.once('ready', (cachedResponse: Buffer | null) => {
87
+ if (!cachedResponse || !res.socket?.writable) {
88
+ next();
89
+ return;
90
+ }
91
+ res.socket.write(cachedResponse.toString('utf8'));
92
+ });
93
+ }
94
+ }
95
+
96
+ function cacheResponse(key: string, req: Request, res: Response): void {
35
97
  if (!res.socket) {
36
98
  return;
37
99
  }
@@ -46,28 +108,43 @@ function cacheResponse(key, req, res) {
46
108
  const socket = res.socket;
47
109
  const originalSocketWriter = socket.write.bind(socket);
48
110
  const responseRef = new WeakRef(res);
49
- let responseChunks = [];
111
+ let responseChunks: Buffer[] = [];
50
112
  let responseSize = 0;
51
- let errorMessage = null;
52
- const patchedWriter = (chunk, encoding, next) => {
113
+ let errorMessage: string | null = null;
114
+ const patchedWriter = (
115
+ chunk: unknown,
116
+ encoding: BufferEncoding | (() => void),
117
+ next?: (() => void) | ((err?: Error) => void)
118
+ ): boolean => {
53
119
  if (errorMessage || !responseRef.deref()) {
54
120
  responseChunks = [];
55
121
  responseSize = 0;
56
- return originalSocketWriter(chunk, encoding, next);
122
+ return originalSocketWriter(
123
+ chunk as string | Buffer | Uint8Array,
124
+ encoding as BufferEncoding,
125
+ next as (err?: Error) => void
126
+ );
57
127
  }
58
128
 
59
- const buf = Buffer.from(chunk, encoding);
129
+ const buf = Buffer.isBuffer(chunk)
130
+ ? chunk
131
+ : Buffer.from(chunk as string, typeof encoding === 'string' ? encoding : undefined);
60
132
  responseChunks.push(buf);
61
133
  responseSize += buf.length;
62
134
  if (responseSize > MAX_CACHED_PAYLOAD_SIZE_BYTES) {
63
- errorMessage = `The actual response size exceeds ` +
135
+ errorMessage =
136
+ `The actual response size exceeds ` +
64
137
  `the maximum allowed limit of ${MAX_CACHED_PAYLOAD_SIZE_BYTES} bytes`;
65
138
  }
66
- return originalSocketWriter(chunk, encoding, next);
139
+ return originalSocketWriter(
140
+ chunk as string | Buffer | Uint8Array,
141
+ encoding as BufferEncoding,
142
+ next as (err?: Error) => void
143
+ );
67
144
  };
68
- socket.write = patchedWriter;
145
+ socket.write = patchedWriter as typeof socket.write;
69
146
  let didEmitReady = false;
70
- res.once('error', (e) => {
147
+ res.once('error', (e: Error) => {
71
148
  errorMessage = e.message;
72
149
  if (socket.write === patchedWriter) {
73
150
  socket.write = originalSocketWriter;
@@ -76,7 +153,7 @@ function cacheResponse(key, req, res) {
76
153
  if (!IDEMPOTENT_RESPONSES.has(key)) {
77
154
  log.info(
78
155
  `Could not cache the response identified by '${key}'. ` +
79
- `Cache consistency has been damaged`
156
+ `Cache consistency has been damaged`
80
157
  );
81
158
  } else {
82
159
  log.info(`Could not cache the response identified by '${key}': ${errorMessage}`);
@@ -98,15 +175,13 @@ function cacheResponse(key, req, res) {
98
175
  if (!IDEMPOTENT_RESPONSES.has(key)) {
99
176
  log.info(
100
177
  `Could not cache the response identified by '${key}'. ` +
101
- `Cache consistency has been damaged`
178
+ `Cache consistency has been damaged`
102
179
  );
103
180
  } else if (errorMessage) {
104
181
  log.info(`Could not cache the response identified by '${key}': ${errorMessage}`);
105
182
  IDEMPOTENT_RESPONSES.delete(key);
106
183
  }
107
184
 
108
- /** @type {CachedResponse|undefined} */
109
- // @ts-ignore The returned type is ok
110
185
  const value = IDEMPOTENT_RESPONSES.get(key);
111
186
  if (value) {
112
187
  value.response = Buffer.concat(responseChunks);
@@ -124,68 +199,9 @@ function cacheResponse(key, req, res) {
124
199
  }
125
200
 
126
201
  if (!didEmitReady) {
127
- /** @type {CachedResponse|undefined} */
128
- // @ts-ignore The returned type is ok
129
202
  const value = IDEMPOTENT_RESPONSES.get(key);
130
203
  responseStateListener.emit('ready', value?.response ?? null);
131
204
  didEmitReady = true;
132
205
  }
133
206
  });
134
207
  }
135
-
136
- /**
137
- * @param {import('express').Request} req
138
- * @param {import('express').Response} res
139
- */
140
- async function handleIdempotency(req, res, next) {
141
- const keyOrArr = req.headers[IDEMPOTENCY_KEY_HEADER];
142
- if (_.isEmpty(keyOrArr) || !keyOrArr) {
143
- return next();
144
- }
145
-
146
- const key = _.isArray(keyOrArr) ? keyOrArr[0] : keyOrArr;
147
-
148
- log.updateAsyncContext({idempotencyKey: key});
149
-
150
- if (!MONITORED_METHODS.includes(req.method)) {
151
- // GET, DELETE, etc. requests are idempotent by default
152
- // there is no need to cache them
153
- return next();
154
- }
155
-
156
- log.debug(`Request idempotency key: ${key}`);
157
- if (!IDEMPOTENT_RESPONSES.has(key)) {
158
- cacheResponse(key, req, res);
159
- return next();
160
- }
161
-
162
- const {
163
- // @ts-ignore We have asserted the presence of the key above
164
- method, path, response, responseStateListener,
165
- } = IDEMPOTENT_RESPONSES.get(key);
166
- if (req.method !== method || req.path !== path) {
167
- log.warn(`Got two different requests with the same idempotency key '${key}'`);
168
- log.warn('Is the client generating idempotency keys properly?');
169
- return next();
170
- }
171
-
172
- if (response) {
173
- log.info(`The same request with the idempotency key '${key}' has been already processed`);
174
- log.info(`Rerouting its response to the current request`);
175
- if (!res.socket?.writable) {
176
- return next();
177
- }
178
- res.socket.write(response.toString('utf8'));
179
- } else {
180
- log.info(`The same request with the idempotency key '${key}' is being processed`);
181
- log.info(`Waiting for the response to be rerouted to the current request`);
182
- responseStateListener.once('ready', async (/** @type {Buffer?} */ cachedResponse) => {
183
- if (!cachedResponse || !res.socket?.writable) {
184
- return next();
185
- }
186
- res.socket.write(cachedResponse.toString('utf8'));
187
- });
188
- }
189
- }
190
-
191
- export {handleIdempotency};
@@ -0,0 +1,3 @@
1
+ import {logger} from '@appium/support';
2
+
3
+ export const log = logger.getLogger('HTTP');
@@ -0,0 +1,187 @@
1
+ import _ from 'lodash';
2
+ import type {NextFunction, Request, RequestHandler, Response} from 'express';
3
+ import type {IncomingMessage} from 'node:http';
4
+ import type {Duplex} from 'node:stream';
5
+ import {log} from './logger';
6
+ import {errors} from '../protocol';
7
+ export {handleIdempotency} from './idempotency';
8
+ import {match} from 'path-to-regexp';
9
+ import {util} from '@appium/support';
10
+ import {calcSignature} from '../helpers/session';
11
+ import {getResponseForW3CError} from '../protocol/errors';
12
+ import type {StringRecord, WSServer} from '@appium/types';
13
+
14
+ const SESSION_ID_PATTERN = /\/session\/([^/]+)/;
15
+
16
+ /**
17
+ * Basic CORS middleware.
18
+ * Sets permissive CORS headers and responds immediately to `OPTIONS` requests with 200.
19
+ */
20
+ export function allowCrossDomain(req: Request, res: Response, next: NextFunction): void {
21
+ res.header('Access-Control-Allow-Origin', '*');
22
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS, DELETE');
23
+ res.header(
24
+ 'Access-Control-Allow-Headers',
25
+ 'Cache-Control, Pragma, Origin, X-Requested-With, Content-Type, Accept, User-Agent'
26
+ );
27
+
28
+ if (req.method === 'OPTIONS') {
29
+ res.sendStatus(200);
30
+ return;
31
+ }
32
+ next();
33
+ }
34
+
35
+ /**
36
+ * CORS middleware for async execute response endpoints only.
37
+ * Leaves other routes untouched but applies {@link allowCrossDomain} to async response URLs.
38
+ *
39
+ * @param basePath - Server base path (e.g. `/wd/hub` or `/`)
40
+ * @returns Express request handler
41
+ */
42
+ export function allowCrossDomainAsyncExecute(basePath: string): RequestHandler {
43
+ function allowCrossDomainAsyncExecuteHandler(
44
+ req: Request,
45
+ res: Response,
46
+ next: NextFunction
47
+ ): void {
48
+ const receiveAsyncResponseRegExp = new RegExp(
49
+ `${_.escapeRegExp(basePath)}/session/[a-f0-9-]+/(appium/)?receive_async_response`
50
+ );
51
+ if (!receiveAsyncResponseRegExp.test(req.url)) {
52
+ next();
53
+ return;
54
+ }
55
+ allowCrossDomain(req, res, next);
56
+ }
57
+
58
+ return allowCrossDomainAsyncExecuteHandler;
59
+ }
60
+
61
+ /**
62
+ * Populates the logger's async context with request and session metadata.
63
+ * Derives `requestId`, optional session id/signature, and `isSensitive` flag from headers/URL.
64
+ */
65
+ export function handleLogContext(req: Request, _res: Response, next: NextFunction): void {
66
+ const requestId = fetchHeaderValue(req, 'x-request-id') || util.uuidV4();
67
+
68
+ const sessionId = SESSION_ID_PATTERN.exec(req.url)?.[1];
69
+ const sessionInfo = sessionId ? {sessionId, sessionSignature: calcSignature(sessionId)} : {};
70
+ const isSensitiveHeaderValue = fetchHeaderValue(req, 'x-appium-is-sensitive');
71
+
72
+ log.updateAsyncContext(
73
+ {
74
+ requestId,
75
+ ...sessionInfo,
76
+ isSensitive: ['true', '1', 'yes'].includes(_.toLower(isSensitiveHeaderValue)),
77
+ },
78
+ true
79
+ );
80
+
81
+ next();
82
+ }
83
+
84
+ /**
85
+ * Ensures requests default to JSON content-type when none is provided.
86
+ */
87
+ export function defaultToJSONContentType(
88
+ req: Request,
89
+ _res: Response,
90
+ next: NextFunction
91
+ ): void {
92
+ if (!req.headers['content-type']) {
93
+ req.headers['content-type'] = 'application/json; charset=utf-8';
94
+ }
95
+ next();
96
+ }
97
+
98
+ /**
99
+ * Attempts to handle a WebSocket upgrade by matching the request path against registered handlers.
100
+ *
101
+ * @param req - Incoming HTTP request
102
+ * @param socket - Network socket
103
+ * @param head - First packet of the upgraded stream
104
+ * @param webSocketsMapping - Path-to-WebSocket-server mapping
105
+ * @returns `true` if the upgrade was handled; `false` otherwise
106
+ */
107
+ export function tryHandleWebSocketUpgrade(
108
+ req: IncomingMessage,
109
+ socket: Duplex,
110
+ head: Buffer,
111
+ webSocketsMapping: StringRecord<WSServer>
112
+ ): boolean {
113
+ if (_.toLower(req.headers?.upgrade) !== 'websocket') {
114
+ return false;
115
+ }
116
+
117
+ let currentPathname: string;
118
+ try {
119
+ currentPathname = new URL(req.url ?? '', 'http://localhost').pathname;
120
+ } catch {
121
+ currentPathname = req.url ?? '';
122
+ }
123
+ for (const [pathname, wsServer] of _.toPairs(webSocketsMapping)) {
124
+ if (match(pathname)(currentPathname)) {
125
+ wsServer.handleUpgrade(req, socket, head, (ws) => {
126
+ wsServer.emit('connection', ws, req);
127
+ });
128
+ return true;
129
+ }
130
+ }
131
+ log.info(`Did not match the websocket upgrade request at ${currentPathname} to any known route`);
132
+ return false;
133
+ }
134
+
135
+ /**
136
+ * Express middleware wrapper around {@link tryHandleWebSocketUpgrade}.
137
+ * Delegates WebSocket upgrades to the mapping and falls through to `next()` otherwise.
138
+ *
139
+ * @param webSocketsMapping - Path-to-WebSocket-server mapping
140
+ * @returns Express request handler
141
+ */
142
+ export function handleUpgrade(webSocketsMapping: StringRecord<WSServer>): RequestHandler {
143
+ function handleUpgradeMiddleware(req: Request, res: Response, next: NextFunction): void {
144
+ if (tryHandleWebSocketUpgrade(req, req.socket, Buffer.from(''), webSocketsMapping)) {
145
+ return;
146
+ }
147
+ next();
148
+ }
149
+
150
+ return handleUpgradeMiddleware;
151
+ }
152
+
153
+ /**
154
+ * Final error-handling middleware.
155
+ * Logs uncaught errors and returns a W3C-formatted error response unless headers were already sent.
156
+ */
157
+ export function catchAllHandler(
158
+ err: Error,
159
+ _req: Request,
160
+ res: Response,
161
+ next: NextFunction
162
+ ): void {
163
+ if (res.headersSent) {
164
+ next(err);
165
+ return;
166
+ }
167
+
168
+ log.error(`Uncaught error: ${err.message}`);
169
+ const [status, body] = getResponseForW3CError(err);
170
+ res.status(status).json(body);
171
+ }
172
+
173
+ /**
174
+ * 404 handler for unmatched routes.
175
+ * Logs a debug message and responds with `UnknownCommandError` in W3C format.
176
+ */
177
+ export function catch404Handler(req: Request, res: Response): void {
178
+ log.debug(`No route found for ${req.url}`);
179
+ const [status, body] = getResponseForW3CError(new errors.UnknownCommandError());
180
+ res.status(status).json(body);
181
+ }
182
+
183
+ function fetchHeaderValue(req: Request, name: string): string | undefined {
184
+ const value = req.headers[name];
185
+ return _.isArray(value) ? value[0] : (value as string | undefined);
186
+ }
187
+