From e9ddbec7bedc8d10c32c2b2ccac4e61408bc2000 Mon Sep 17 00:00:00 2001 From: "e.khalilov" Date: Tue, 20 Jan 2026 14:38:29 +0300 Subject: [PATCH 1/2] fix compose+logs && rewrite to ts --- docker-compose-macos.yaml | 8 +- docker-compose-prod.yaml | 8 +- lib/cli/local/index.js | 4 +- lib/units/base-device/support/storage.js | 93 ------- lib/units/base-device/support/storage.ts | 174 ++++++++++++ .../device/plugins/{install.js => install.ts} | 96 ++++--- lib/units/storage/plugins/apk/index.js | 66 ----- lib/units/storage/plugins/apk/index.ts | 128 +++++++++ lib/units/storage/s3.js | 139 ---------- lib/units/storage/s3.ts | 166 ++++++++++++ lib/units/storage/temp.js | 197 -------------- lib/units/storage/temp.ts | 255 ++++++++++++++++++ package-lock.json | 40 +++ package.json | 3 + 14 files changed, 835 insertions(+), 542 deletions(-) delete mode 100644 lib/units/base-device/support/storage.js create mode 100644 lib/units/base-device/support/storage.ts rename lib/units/device/plugins/{install.js => install.ts} (73%) delete mode 100644 lib/units/storage/plugins/apk/index.js create mode 100644 lib/units/storage/plugins/apk/index.ts delete mode 100644 lib/units/storage/s3.js create mode 100644 lib/units/storage/s3.ts delete mode 100644 lib/units/storage/temp.js create mode 100644 lib/units/storage/temp.ts diff --git a/docker-compose-macos.yaml b/docker-compose-macos.yaml index f61f37477b..2d0840e20a 100644 --- a/docker-compose-macos.yaml +++ b/docker-compose-macos.yaml @@ -134,7 +134,7 @@ services: container_name: devicehub-storage-plugin-apk env_file: - scripts/variables.env - command: stf storage-plugin-apk --port 3000 --storage-url http://devicehub-storage-temp/ --secret=${STF_SECRET} + command: stf storage-plugin-apk --port 3000 --storage-url http://devicehub-storage-temp:3000/ --secret=${STF_SECRET} depends_on: devicehub-migrate: condition: service_completed_successfully @@ -147,7 +147,7 @@ services: container_name: devicehub-storage-plugin-image env_file: - scripts/variables.env - command: stf storage-plugin-image --port 3000 --storage-url https://${STF_DOMAIN}:${STF_PORT}/ --secret=${STF_SECRET} + command: stf storage-plugin-image --port 3000 --storage-url http://devicehub-storage-temp:3000/ --secret=${STF_SECRET} depends_on: devicehub-migrate: condition: service_completed_successfully @@ -207,7 +207,7 @@ services: container_name: devicehub-websocket env_file: - scripts/variables.env - command: stf websocket --port 3000 --storage-url https://${STF_DOMAIN}:${STF_PORT}/ --connect-sub tcp://devicehub-triproxy-app:7150 --connect-push tcp://devicehub-triproxy-app:7170 --connect-sub-dev tcp://devicehub-triproxy-dev:7250 --connect-push-dev tcp://devicehub-triproxy-dev:7270 --secret=${STF_SECRET} + command: stf websocket --port 3000 --storage-url http://devicehub-storage-temp:3000/ --connect-sub tcp://devicehub-triproxy-app:7150 --connect-push tcp://devicehub-triproxy-app:7170 --connect-sub-dev tcp://devicehub-triproxy-dev:7250 --connect-push-dev tcp://devicehub-triproxy-dev:7270 --secret=${STF_SECRET} depends_on: devicehub-migrate: condition: service_completed_successfully @@ -252,7 +252,7 @@ services: devicehub-migrate: condition: service_completed_successfully restart: unless-stopped - command: stf provider --name devicehub-provider --adb-host host.docker.internal --no-cleanup --connect-sub tcp://devicehub-triproxy-dev:7250 --connect-push tcp://devicehub-triproxy-dev:7270 --storage-url https://${STF_DOMAIN}:${STF_PORT}/ --public-ip ${STF_DOMAIN} --min-port=12010 --max-port=12100 --heartbeat-interval 10000 --screen-ws-url-pattern "wss://${STF_DOMAIN}:${STF_PORT}/d/devicehub-provider/<%= publicPort %>/" --secret=${STF_SECRET} + command: stf provider --name devicehub-provider --adb-host host.docker.internal --no-cleanup --connect-sub tcp://devicehub-triproxy-dev:7250 --connect-push tcp://devicehub-triproxy-dev:7270 --storage-url http://devicehub-storage-plugin-apk:3000/ --public-ip ${STF_DOMAIN} --min-port=12010 --max-port=12100 --heartbeat-interval 10000 --screen-ws-url-pattern "wss://${STF_DOMAIN}:${STF_PORT}/d/devicehub-provider/<%= publicPort %>/" --secret=${STF_SECRET} networks: devicehub: devicehub-ssl: diff --git a/docker-compose-prod.yaml b/docker-compose-prod.yaml index acdd35dc65..123ce8ded9 100644 --- a/docker-compose-prod.yaml +++ b/docker-compose-prod.yaml @@ -150,7 +150,7 @@ services: container_name: devicehub-storage-plugin-apk env_file: - scripts/variables.env - command: stf storage-plugin-apk --port 3000 --storage-url http://devicehub-storage-temp/ --secret=${STF_SECRET} + command: stf storage-plugin-apk --port 3000 --storage-url http://devicehub-storage-temp:3000/ --secret=${STF_SECRET} depends_on: devicehub-migrate: condition: service_completed_successfully @@ -163,7 +163,7 @@ services: container_name: devicehub-storage-plugin-image env_file: - scripts/variables.env - command: stf storage-plugin-image --port 3000 --storage-url https://${STF_DOMAIN}:${STF_PORT}/ --secret=${STF_SECRET} + command: stf storage-plugin-image --port 3000 --storage-url http://devicehub-storage-temp:3000/ --secret=${STF_SECRET} depends_on: devicehub-migrate: condition: service_completed_successfully @@ -223,7 +223,7 @@ services: container_name: devicehub-websocket env_file: - scripts/variables.env - command: stf websocket --port 3000 --storage-url https://${STF_DOMAIN}:${STF_PORT}/ --connect-sub tcp://devicehub-triproxy-app:7150 --connect-push tcp://devicehub-triproxy-app:7170 --connect-sub-dev tcp://devicehub-triproxy-dev:7250 --connect-push-dev tcp://devicehub-triproxy-dev:7270 --secret=${STF_SECRET} + command: stf websocket --port 3000 --storage-url http://devicehub-storage-temp:3000/ --connect-sub tcp://devicehub-triproxy-app:7150 --connect-push tcp://devicehub-triproxy-app:7170 --connect-sub-dev tcp://devicehub-triproxy-dev:7250 --connect-push-dev tcp://devicehub-triproxy-dev:7270 --secret=${STF_SECRET} depends_on: devicehub-migrate: condition: service_completed_successfully @@ -270,7 +270,7 @@ services: adbd: condition: service_healthy restart: unless-stopped - command: stf provider --name devicehub-provider --adb-host adbd --no-cleanup --connect-sub tcp://devicehub-triproxy-dev:7250 --connect-push tcp://devicehub-triproxy-dev:7270 --storage-url https://${STF_DOMAIN}:${STF_PORT}/ --public-ip ${STF_DOMAIN} --min-port=12010 --max-port=12100 --heartbeat-interval 10000 --screen-ws-url-pattern "wss://${STF_DOMAIN}:${STF_PORT}/d/devicehub-provider/<%= publicPort %>/" --secret=${STF_SECRET} + command: stf provider --name devicehub-provider --adb-host adbd --no-cleanup --connect-sub tcp://devicehub-triproxy-dev:7250 --connect-push tcp://devicehub-triproxy-dev:7270 --storage-url http://devicehub-storage-plugin-apk:3000/ --public-ip ${STF_DOMAIN} --min-port=12010 --max-port=12100 --heartbeat-interval 10000 --screen-ws-url-pattern "wss://${STF_DOMAIN}:${STF_PORT}/d/devicehub-provider/<%= publicPort %>/" --secret=${STF_SECRET} networks: devicehub: devicehub-ssl: diff --git a/lib/cli/local/index.js b/lib/cli/local/index.js index 83e57c0b94..2eed033d86 100644 --- a/lib/cli/local/index.js +++ b/lib/cli/local/index.js @@ -444,14 +444,14 @@ export const handler = function(argv) { 'storage-plugin-image', '--port', argv.storagePluginImagePort, '--storage-url', - util.format('http://localhost:%d/', argv.port), + util.format('http://localhost:%d/', argv.storagePort), '--secret', argv.authSecret ], [ // apk processor 'storage-plugin-apk', '--port', argv.storagePluginApkPort, '--storage-url', - util.format('http://localhost:%d/', argv.port), + util.format('http://localhost:%d/', argv.storagePort), '--secret', argv.authSecret ] ], diff --git a/lib/units/base-device/support/storage.js b/lib/units/base-device/support/storage.js deleted file mode 100644 index f7719933c4..0000000000 --- a/lib/units/base-device/support/storage.js +++ /dev/null @@ -1,93 +0,0 @@ -import util from 'util' -import url from 'url' -import syrup from '@devicefarmer/stf-syrup' -import request from 'postman-request' -import logger from '../../../util/logger.js' -import {createReadStream, createWriteStream} from 'fs' -import {unlink} from 'fs/promises' -import {Readable} from 'stream' -import temp from 'tmp-promise' -import {finished} from 'stream/promises' - -export default syrup.serial() - .define(options => { - const log = logger.createLogger('base-device:support:storage') - const plugin = { - store: (type, stream, meta) => new Promise((resolve, reject) => { - const args = { - url: url.resolve(options.storageUrl, util.format('s/upload/%s', type)), - headers: { - internal: 'Internal ' + meta.jwt - } - } - const req = request.post(args, (err, res, body) => { - try { - if (err) { - log.error('Upload to "%s" failed', args.url, err.stack) - reject(err) - return - } - - if (res.statusCode !== 201) { - log.error('Upload to "%s" failed: HTTP %d', args.url, res.statusCode) - log.debug(body) - reject( - new Error(util.format('Upload to "%s" failed: HTTP %d', args.url, res.statusCode)) - ) - return - } - - const result = JSON.parse(body) - log.info('Uploaded to "%s"', result.resources.file.href) - resolve(result.resources.file) - } - catch (/** @type {any} */ err) { - log.error('Invalid JSON in response', err.stack, body) - reject(err) - } - }) - - req.form().append('file', stream, meta) - }), - - storeByPath: (path, type, meta) => - plugin.store(type, createReadStream(path), meta), - - get: async(href, channel, jwt) => { - const apkUrl = url.resolve(options.storageUrl, href) - const res = await fetch(apkUrl, { - headers: { - channel, Authorization: `Bearer ${jwt}`, - device: options.serial - } - }) - - log.info('Reading', apkUrl, ' returned: ', res.status) - if (res.status >= 300) { - throw Error(`Could not download file. Server returned status = ${res.status}, ${await res.text()}`) - } - if (res.body === null) { - throw Error(`Could not download file. Server returned no body and status = ${res.status}`) - } - - return Readable.fromWeb(res.body) - }, - - download: async(href, channel, jwt, localPath, name) => { - const fileStream = await plugin.get(href, channel, jwt) - const file = (localPath && {path: localPath}) || await temp.file(name ? {name} : {}) - const writeStream = createWriteStream(file.path) - - log.info(`Downloading to ${file.path}`) - - await finished(fileStream.pipe(writeStream)) - - return { - path: file.path, - cleanup: () => - file?.cleanup ? file.cleanup() : unlink(file.path) - } - } - } - return plugin - }) diff --git a/lib/units/base-device/support/storage.ts b/lib/units/base-device/support/storage.ts new file mode 100644 index 0000000000..90f5a725e8 --- /dev/null +++ b/lib/units/base-device/support/storage.ts @@ -0,0 +1,174 @@ +import util from 'util' +import url from 'url' +import syrup from '@devicefarmer/stf-syrup' +import logger from '../../../util/logger.js' +import { createReadStream, createWriteStream } from 'fs' +import { unlink } from 'fs/promises' +import temp from 'tmp-promise' +import { finished } from 'stream/promises' +import https from "https" +import http from "http" +import FormData from 'form-data' +import { Readable } from 'stream' + +interface StorageOptions { + storageUrl: string + serial: string +} + +interface FileMetadata { + jwt: string + filename?: string + contentType?: string + [key: string]: any +} + +interface UploadedFile { + href: string + [key: string]: any +} + +interface DownloadedFile { + path: string + cleanup: () => Promise +} + +interface UploadOptions { + url: string + headers?: Record +} + +function uploadFile( + options: UploadOptions, + stream: Readable, + meta: FileMetadata +): Promise<{ statusCode: number; body: string }> { + return new Promise((resolve, reject) => { + const parsedUrl = new URL(options.url) + const protocol = parsedUrl.protocol === 'https:' ? https : http + + const form = new FormData() + form.append('file', stream, meta) + + const requestOptions: http.RequestOptions = { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.pathname + parsedUrl.search, + method: 'POST', + headers: { + ...options.headers, + ...form.getHeaders() + } + } + + const req = protocol.request(requestOptions, (res) => { + const chunks: Buffer[] = [] + + res.on('data', (chunk) => { + chunks.push(chunk) + }) + + res.on('end', () => { + const body = Buffer.concat(chunks).toString() + resolve({ + statusCode: res.statusCode || 500, + body + }) + }) + }) + + req.on('error', (err) => { + reject(err) + }) + + form.pipe(req) + }) +} + +export default syrup.serial() + .define((options: StorageOptions) => { + const log = logger.createLogger('base-device:support:storage') + + const plugin = { + store: async (type: string, stream: Readable, meta: FileMetadata): Promise => { + const uploadUrl = url.resolve(options.storageUrl, util.format('s/upload/%s', type)) + const headers = { + internal: 'Internal ' + meta.jwt + } + + try { + const { statusCode, body } = await uploadFile( + { url: uploadUrl, headers }, + stream, + meta + ) + + if (statusCode !== 201) { + log.error('Upload to %s failed: HTTP %s', uploadUrl, statusCode) + log.debug(body) + throw new Error(util.format('Upload to %s failed: HTTP %s', uploadUrl, statusCode)) + } + + const result = JSON.parse(body) + log.info('Uploaded to %s', result.resources.file.href) + return result.resources.file + } catch (err: any) { + if (err instanceof SyntaxError) { + log.error('Invalid JSON in response, err: %s', err) + throw err + } + log.error('Upload to %s failed: %s', uploadUrl, err.stack) + throw err + } + }, + + storeByPath: (path: string, type: string, meta: FileMetadata): Promise => + plugin.store(type, createReadStream(path), meta), + + get: async (href: string, channel: string, jwt: string): Promise => { + const apkUrl = url.resolve(options.storageUrl, href) + const res = await fetch(apkUrl, { + headers: { + channel, + Authorization: `Bearer ${jwt}`, + device: options.serial + } + }) + + log.info('Reading %s returned: %s', apkUrl, res.status) + + if (res.status >= 300) { + throw Error(`Could not download file. Server returned status = ${res.status}, ${await res.text()}`) + } + if (res.body === null) { + throw Error(`Could not download file. Server returned no body and status = ${res.status}`) + } + + return Readable.fromWeb(res.body as any) + }, + + download: async ( + href: string, + channel: string, + jwt: string, + localPath?: string, + name?: string + ): Promise => { + const fileStream = await plugin.get(href, channel, jwt) + const file = (localPath && { path: localPath }) || await temp.file(name ? { name } : {}) + const writeStream = createWriteStream(file.path) + + log.info('Downloading to %s', file.path) + + await finished(fileStream.pipe(writeStream)) + + return { + path: file.path, + cleanup: () => + (file as any)?.cleanup ? (file as any).cleanup() : unlink(file.path) + } + } + } + + return plugin + }) diff --git a/lib/units/device/plugins/install.js b/lib/units/device/plugins/install.ts similarity index 73% rename from lib/units/device/plugins/install.js rename to lib/units/device/plugins/install.ts index 9eae1703d2..bd68ca93a2 100644 --- a/lib/units/device/plugins/install.js +++ b/lib/units/device/plugins/install.ts @@ -1,9 +1,7 @@ import fs from 'fs/promises' import util from 'util' import syrup from '@devicefarmer/stf-syrup' -import Bluebird from 'bluebird' import logger from '../../../util/logger.js' -import wire from '../../../wire/index.js' import wireutil from '../../../wire/util.js' import * as promiseutil from '../../../util/promiseutil.js' import {Utils} from '@u4/adbkit' @@ -11,30 +9,41 @@ import adb from '../support/adb.js' import router from '../../base-device/support/router.js' import push from '../../base-device/support/push.js' import storage from '../../base-device/support/storage.js' -import {InstallMessage, UninstallMessage} from '../../../wire/wire.js' - -// @ts-ignore -const readAll = async(stream) => Utils.readAll(stream) +import {InstallMessage, InstallResultMessage, UninstallMessage} from '../../../wire/wire.js' + +interface InstallOptions { + serial: string + [key: string]: any +} + +interface Manifest { + package: string + application: { + launcherActivities: any[] + [key: string]: any + } + [key: string]: any +} export default syrup.serial() .dependency(adb) .dependency(router) .dependency(push) .dependency(storage) - .define(function(options, adb, router, push, storage) { + .define((options: InstallOptions, adb: any, router: any, push: any, storage: any) => { const log = logger.createLogger('device:plugins:install') const reply = wireutil.reply(options.serial) - router.on(InstallMessage, async(channel, message) => { - const manifest = JSON.parse(message.manifest) + router.on(InstallMessage, async (channel: string, message: any) => { + const manifest: Manifest = JSON.parse(message.manifest) const pkg = manifest.package - const installFlags = message.installFlags - const isApi = message.isApi - const jwt = message.jwt + const installFlags: string[] = message.installFlags + const isApi: boolean = message.isApi + const jwt: string = message.jwt log.info('Installing package "%s" from "%s"', pkg, message.href) - const sendProgress = (data, progress) => { + const sendProgress = (data: string, progress: number): void => { if (!isApi) { push.send([ channel, @@ -43,10 +52,7 @@ export default syrup.serial() } } - /** - * @returns {Promise} - */ - const pushApp = async(channel) => { + const pushApp = async (channel: string): Promise => { try { const {path, cleanup} = await storage.download(message.href, channel, jwt) const stats = await fs.stat(path) @@ -58,8 +64,8 @@ export default syrup.serial() const transfer = await adb.getDevice(options.serial) .push(path, target, 0o755) - let transferError - transfer.on('error', (error) => transferError = error) // work? + let transferError: Error | undefined + transfer.on('error', (error: Error) => transferError = error) await transfer.waitForEnd() if (transferError) { @@ -86,20 +92,20 @@ export default syrup.serial() return target } - catch (/** @type {any}*/error) { + catch (error: any) { await cleanup() log.error(`Failed to verify pushed file: ${error?.message || error}`) } } - catch (err) { - log.error('Pushing file on device failed:', err) + catch (err: any) { + log.error('Pushing file on device failed: %s', err) } } - const install = async(installCmd) => { + const install = async (installCmd: string, attempt = 0): Promise => { try { const r = await adb.getDevice(options.serial).shell(installCmd) - const buffer = await readAll(r) + const buffer = await Utils.readAll(r) const result = buffer.toString() log.info('Installing result ' + result) if (result.includes('Success')) { @@ -109,28 +115,42 @@ export default syrup.serial() ]) push.send([ channel, - wireutil.envelope(new wire.InstallResultMessage(options.serial, 'Installed successfully')) + wireutil.pack(InstallResultMessage, { + serial: options.serial, + result: 'Installed successfully' + }) ]) } else { if (result.includes('INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES') || result.includes('INSTALL_FAILED_VERSION_DOWNGRADE')) { + if (attempt) { + throw new Error(result) + } + log.info('Uninstalling "%s" first due to inconsistent certificates', pkg) - adb.getDevice(options.serial).uninstall(pkg) - .then(() => adb.getDevice(options.serial).shell(installCmd)) + await adb.getDevice(options.serial).uninstall(pkg) + return install(installCmd, 1) } else { log.error('Tried to install package "%s", got "%s"', pkg, result) + push.send([ channel, reply.fail(result) ]) + push.send([ channel, - wireutil.envelope(new wire.InstallResultMessage(options.serial, `Tried to install package ${pkg}, got ${result}`)) + wireutil.pack(InstallResultMessage, { + serial: options.serial, + result: `Tried to install package ${pkg}, got ${result}` + }) ]) + throw new Error(result) } } + if (message.launch) { if (manifest.application.launcherActivities.length) { // According to the AndroidManifest.xml documentation the dot is @@ -142,7 +162,9 @@ export default syrup.serial() category: ['android.intent.category.LAUNCHER'], flags: 0x10200000 } + log.info('Launching activity with action "%s" on component "%s"', launchActivity.action, launchActivity.component) + // Progress 90% sendProgress('launching_app', 90) @@ -153,7 +175,7 @@ export default syrup.serial() } } } - catch (err) { + catch (err: any) { log.error('Error while installation \n') log.error(err) throw err @@ -174,15 +196,15 @@ export default syrup.serial() log.info('Install command: ' + installCmd) sendProgress('installing_app', 50) - await promiseutil.periodicNotify( install(installCmd), 250 ) } - catch (err) { - if (err instanceof Bluebird.TimeoutError) { - log.error('Installation of package "%s" failed', pkg, err.stack) + catch (err: any) { + // Check for timeout-like errors + if (err?.name === 'TimeoutError' || err?.message?.includes('timeout')) { + log.error('Installation of package "%s" failed: %s', pkg, err.stack) push.send([ channel, reply.fail('INSTALL_ERROR_TIMEOUT') @@ -190,7 +212,7 @@ export default syrup.serial() return } - log.error('Installation of package "%s" failed', pkg, err.stack) + log.error('Installation of package "%s" failed: %s', pkg, err) push.send([ channel, reply.fail('INSTALL_ERROR_UNKNOWN') @@ -198,7 +220,7 @@ export default syrup.serial() } }) - router.on(UninstallMessage, async(channel, message) => { + router.on(UninstallMessage, async (channel: string, message: any) => { log.info('Uninstalling "%s"', message.packageName) try { await adb.getDevice(options.serial).uninstall(message.packageName) @@ -207,8 +229,8 @@ export default syrup.serial() reply.okay('success') ]) } - catch (err) { - log.error('Uninstallation failed', err.stack) + catch (err: any) { + log.error('Uninstallation failed: %s', err) push.send([ channel, reply.fail('fail') diff --git a/lib/units/storage/plugins/apk/index.js b/lib/units/storage/plugins/apk/index.js deleted file mode 100644 index 1bf6329a51..0000000000 --- a/lib/units/storage/plugins/apk/index.js +++ /dev/null @@ -1,66 +0,0 @@ -import http from 'http' -import url from 'url' -import util from 'util' -import express from 'express' -import request from 'postman-request' -import logger from '../../../../util/logger.js' -import download from '../../../../util/download.js' -import manifest from './task/manifest.js' -import rateLimitConfig from '../../../ratelimit/index.js' -import {accessTokenAuth} from '../../../api/helpers/securityHandlers.js' -import cookieSession from 'cookie-session' -import csrf from 'csurf' -import * as apiutil from '../../../../util/apiutil.js' -export default (function(options) { - var log = logger.createLogger('storage:plugins:apk') - var app = express() - var server = http.createServer(app) - // eslint-disable-next-line new-cap - const route = express.Router() - log.info('cacheDir located at ' + options.cacheDir) - app.use(rateLimitConfig) - app.use(cookieSession({ - name: options.ssid, - keys: [options.secret] - })) - app.use(csrf()) - app.use(route) - app.set('strict routing', true) - app.set('case sensitive routing', true) - app.set('trust proxy', true) - route.get('/s/apk/:id/:name/manifest', function(req, res) { - var orig = util.format('/s/blob/%s/%s', req.params.id, req.params.name) - let downloadUrl = url.resolve(options.storageUrl, orig) - log.info(`Downloading apk from ${downloadUrl}`) - download(downloadUrl, { - dir: options.cacheDir - }, req.headers) - .then((file) => { - log.info('Got apk from ' + downloadUrl + ' in ' + file.path) - return manifest(file).then(data => { - res.status(200) - .json({ - success: true, - manifest: data - }) - }) - }) - .catch(function(err) { - log.error('Unable to read manifest of "%s"', req.params.id, err) - res.status(400) - .json({ - success: false - }) - }) - }) - route.get('/s/apk/:id/:name', function(req, res) { - request(url.resolve(options.storageUrl, util.format('/s/blob/%s/%s', req.params.id, req.params.name)), { - timeout: apiutil.INSTALL_APK_WAIT, - pool: {maxSockets: Infinity}, - headers: req.headers - }) - .pipe(res) - }) - server.listen(options.port) - log.info('Listening on port %d', options.port) -}) diff --git a/lib/units/storage/plugins/apk/index.ts b/lib/units/storage/plugins/apk/index.ts new file mode 100644 index 0000000000..30e45bc368 --- /dev/null +++ b/lib/units/storage/plugins/apk/index.ts @@ -0,0 +1,128 @@ +import http from 'http' +import https from 'https' +import url from 'url' +import util from 'util' +import express, {Response, Router} from 'express' +import logger from '../../../../util/logger.js' +import download from '../../../../util/download.js' +import manifest from './task/manifest.js' +import rateLimitConfig from '../../../ratelimit/index.js' +import cookieSession from 'cookie-session' +import csrf from 'csurf' +import * as apiutil from '../../../../util/apiutil.js' + +interface ApkPluginOptions { + cacheDir: string + storageUrl: string + port: number + ssid: string + secret: string +} + + +/** + * Proxy HTTP/HTTPS request and pipe to response + */ +function proxyRequest( + targetUrl: string, + headers: http.IncomingHttpHeaders, + res: Response, + timeout?: number +): void { + const parsedUrl = new URL(targetUrl) + const protocol = parsedUrl.protocol === 'https:' ? https : http + + const options: http.RequestOptions = { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.pathname + parsedUrl.search, + method: 'GET', + headers: { + ...headers, + host: parsedUrl.host + }, + ...(timeout && { timeout }) + } + + const proxyReq = protocol.request(options, (proxyRes) => { + // Forward status code and headers + res.status(proxyRes.statusCode || 200) + + if (proxyRes.headers) { + Object.entries(proxyRes.headers).forEach(([key, value]) => { + if (value !== undefined) { + res.setHeader(key, value) + } + }) + } + + // Pipe the response + proxyRes.pipe(res) + }) + + proxyReq.on('error', (err) => { + if (!res.headersSent) { + res.status(500).json({ + success: false, + error: 'Failed to proxy request' + }) + } + }) + + proxyReq.end() +} + +export default function(options: ApkPluginOptions): void { + const log = logger.createLogger('storage:plugins:apk') + const app = express() + const server = http.createServer(app) + const route: Router = express.Router() + + log.info('cacheDir located at ' + options.cacheDir) + + app.use(rateLimitConfig) + app.use(cookieSession({ + name: options.ssid, + keys: [options.secret] + })) + app.use(csrf()) + app.use(route) + app.set('strict routing', true) + app.set('case sensitive routing', true) + app.set('trust proxy', true) + + route.get('/s/apk/:id/:name/manifest', async(req, res) => { + const orig = util.format('/s/blob/%s/%s', req.params.id, req.params.name) + const downloadUrl = url.resolve(options.storageUrl, orig) + + log.info(`Downloading apk from ${downloadUrl}`) + + try { + const file = await download(downloadUrl, { + dir: options.cacheDir + }, req.headers) + + log.info('Got apk from %s in %s', downloadUrl, file.path) + const data = await manifest(file) + + res.status(200).json({ + success: true, + manifest: data + }) + } catch (err) { + log.error('Unable to read manifest of "%s": %s', req.params.id, err) + res.status(400).json({success: false}) + } + }) + + route.get('/s/apk/:id/:name', (req, res) => { + const targetUrl = url.resolve( + options.storageUrl, + util.format('/s/blob/%s/%s', req.params.id, req.params.name) + ) + proxyRequest(targetUrl, req.headers, res, apiutil.INSTALL_APK_WAIT) + }) + + server.listen(options.port) + log.info('Listening on port %s', options.port) +} diff --git a/lib/units/storage/s3.js b/lib/units/storage/s3.js deleted file mode 100644 index eb82a5ba4f..0000000000 --- a/lib/units/storage/s3.js +++ /dev/null @@ -1,139 +0,0 @@ -import http from 'http' -import util from 'util' -import path from 'path' -import fs from 'fs' -import express from 'express' -import validator from 'express-validator' -import bodyParser from 'body-parser' -import formidable from 'formidable' -import Promise from 'bluebird' -import {v4 as uuidv4} from 'uuid' -import {fromIni} from '@aws-sdk/credential-providers' -import {S3} from '@aws-sdk/client-s3' -import logger from '../../util/logger.js' -import rateLimitConfig from '../ratelimit/index.js' -import {accessTokenAuth} from '../api/helpers/securityHandlers.js' -export default (function(options) { - var log = logger.createLogger('storage:s3') - var app = express() - var server = http.createServer(app) - var s3 = new S3({ - credentials: // JS SDK v3 switched credential providers from classes to functions. - // This is the closest approximation from codemod of what your application needs. - // Reference: https://www.npmjs.com/package/@aws-sdk/credential-providers - fromIni({ - profile: options.profile - }), - endpoint: options.endpoint - }) - app.set('strict routing', true) - app.set('case sensitive routing', true) - app.set('trust proxy', true) - app.use(rateLimitConfig) - app.use(bodyParser.json()) - app.use(validator()) - function putObject(plugin, file) { - return new Promise(function(resolve, reject) { - var id = uuidv4() - s3.putObject({ - Key: id, - Body: fs.createReadStream(file.path), - Bucket: options.bucket, - Metadata: { - plugin: plugin, - name: file.name - } - }, function(err) { - if (err) { - log.error('Unable to store "%s" as "%s/%s"', file.temppath, options.bucket, id, err.stack) - reject(err) - } - else { - log.info('Stored "%s" as "%s/%s"', file.name, options.bucket, id) - resolve(id) - } - }) - }) - } - function getHref(plugin, id, name) { - return util.format('/s/%s/%s%s', plugin, id, name ? '/' + path.basename(name) : '') - } - app.post('/s/upload/:plugin', function(req, res) { - var form = new formidable.IncomingForm({ - maxFileSize: options.maxFileSize - }) - var plugin = req.params.plugin - Promise.promisify(form.parse, form)(req) - .spread(function(fields, files) { - var requests = Object.keys(files).map(function(field) { - var file = files[field] - return putObject(plugin, file) - .then(function(id) { - log.info('Store screenshot :', file.path, file.name) - return { - field: field, - id: id, - name: file.name, - temppath: file.path - } - }) - }) - return Promise.all(requests) - }) - .then(function(storedFiles) { - res.status(201).json({ - success: true, - resources: (function() { - var mapped = Object.create(null) - storedFiles.forEach(function(file) { - mapped[file.field] = { - date: new Date(), - plugin: plugin, - id: file.id, - name: file.name, - href: getHref(plugin, file.id, file.name) - } - }) - return mapped - })() - }) - return storedFiles - }) - .then(function(storedFiles) { - return Promise.all(storedFiles.map(function(file) { - return Promise.promisify(fs.unlink, fs)(file.temppath) - .catch(function(err) { - log.warn('Unable to clean up "%s"', file.temppath, err.stack) - return true - }) - })) - }) - .catch(function(err) { - log.error('Error storing resource', err.stack) - res.status(500) - .json({ - success: false, - error: 'ServerError' - }) - }) - }) - app.get('/s/blob/:id/:name', function(req, res) { - var params = { - Key: req.params.id, - Bucket: options.bucket - } - s3.getObject(params, function(err, data) { - if (err) { - log.error('Unable to retrieve "%s"', path, err.stack) - res.sendStatus(404) - return - } - res.set({ - 'Content-Type': data.ContentType - }) - res.send(data.Body) - }) - }) - server.listen(options.port) - log.info('Listening on port %d', options.port) -}) diff --git a/lib/units/storage/s3.ts b/lib/units/storage/s3.ts new file mode 100644 index 0000000000..bbe81dd73b --- /dev/null +++ b/lib/units/storage/s3.ts @@ -0,0 +1,166 @@ +import http from 'http' +import util from 'util' +import path from 'path' +import fs from 'fs' +import express from 'express' +import validator from 'express-validator' +import bodyParser from 'body-parser' +import { v4 as uuidv4 } from 'uuid' +import { fromIni } from '@aws-sdk/credential-providers' +import { S3 } from '@aws-sdk/client-s3' +import logger from '../../util/logger.js' +import rateLimitConfig from '../ratelimit/index.js' + +// @ts-ignore +import formidable from 'formidable' + +interface S3Options { + profile: string + endpoint?: string + bucket: string + maxFileSize: number + port: number +} + +interface UploadedFile { + field: string + id: string + name: string + temppath: string +} + +interface ResourceResponse { + date: Date + plugin: string + id: string + name: string + href: string +} + +export default function (options: S3Options) { + const log = logger.createLogger('storage:s3') + const app = express() + const server = http.createServer(app) + const s3 = new S3({ + // JS SDK v3 switched credential providers from classes to functions. + // This is the closest approximation from codemod of what your application needs. + // Reference: https://www.npmjs.com/package/@aws-sdk/credential-providers + credentials: fromIni({ + profile: options.profile + }), + endpoint: options.endpoint + }) + + app.set('strict routing', true) + app.set('case sensitive routing', true) + app.set('trust proxy', true) + app.use(rateLimitConfig) + app.use(bodyParser.json()) + app.use(validator()) + + const putObject = async(plugin: string, file: any): Promise => { + const id = uuidv4() + try { + await s3.putObject({ + Key: id, + Body: fs.createReadStream(file.path), + Bucket: options.bucket, + Metadata: { + plugin: plugin, + name: file.name + } + }) + + log.info('Stored %s as %s/%s', file.name, options.bucket, id) + return id + } catch (err: any) { + log.error('Unable to store %s as %s/%s: %s', file.temppath, options.bucket, id, err.stack) + throw err + } + } + + const getHref = (plugin: string, id: string, name: string) => + util.format('/s/%s/%s%s', plugin, id, name ? '/' + path.basename(name) : '') + + app.post('/s/upload/:plugin', async (req, res) => { + try { + const form = new formidable.IncomingForm({ + maxFileSize: options.maxFileSize + } as any) + + const plugin = req.params.plugin + const parseForm = util.promisify(form.parse.bind(form)) + const [fields, files] = (await parseForm(req)) as any + + const uploadRequests = Object.keys(files).map(async (field) => { + const file = files[field] + const id = await putObject(plugin, file) + log.info('Store screenshot: %s %s', file.path, file.name) + return { + field: field, + id: id, + name: file.name, + temppath: file.path + } + }) + + const storedFiles: UploadedFile[] = await Promise.all(uploadRequests) + + const mapped: Record = Object.create(null) + storedFiles.forEach((file) => { + mapped[file.field] = { + date: new Date(), + plugin: plugin, + id: file.id, + name: file.name, + href: getHref(plugin, file.id, file.name) + } + }) + + res.status(201).json({ + success: true, + resources: mapped + }) + + // Cleanup temp files + const unlinkAsync = util.promisify(fs.unlink) + await Promise.all( + storedFiles.map(async (file) => { + try { + await unlinkAsync(file.temppath) + } catch (err: any) { + log.warn('Unable to clean up %s: %s', file.temppath, err.stack) + } + }) + ) + } catch (err: any) { + log.error('Error storing resource %s', err.stack) + res.status(500).json({ + success: false, + error: 'ServerError' + }) + } + }) + + app.get('/s/blob/:id/:name', async (req, res) => { + try { + const params = { + Key: req.params.id, + Bucket: options.bucket + } + + const data = await s3.getObject(params) + + res.set({ + 'Content-Type': data.ContentType + }) + res.send(data.Body) + } catch (err: any) { + log.error('Unable to retrieve %s: %s', req.params.id, err.stack) + res.sendStatus(404) + } + }) + + server.listen(options.port) + log.info('Listening on port %s', options.port) +} diff --git a/lib/units/storage/temp.js b/lib/units/storage/temp.js deleted file mode 100644 index c22aa8bf40..0000000000 --- a/lib/units/storage/temp.js +++ /dev/null @@ -1,197 +0,0 @@ -import http from 'http' -import util from 'util' -import path from 'path' -import crypto from 'crypto' -import express from 'express' -import validator from 'express-validator' -import bodyParser from 'body-parser' -import formidable from 'formidable' -import Promise from 'bluebird' -import cookieParser from 'cookie-parser' -import logger from '../../util/logger.js' -import Storage from '../../util/storage.js' -import * as requtil from '../../util/requtil.js' -import download from '../../util/download.js' -import bundletool from '../../util/bundletool.js' -import rateLimitConfig from '../ratelimit/index.js' -import {accessTokenAuth} from '../api/helpers/securityHandlers.js' -import cookieSession from 'cookie-session' -import db from '../../db/index.js' - -export default (async function(options) { - await db.connect() - - const log = logger.createLogger('storage:temp') - const app = express() - const server = http.createServer(app) - const storage = new Storage() - // eslint-disable-next-line new-cap - const route = express.Router() - app.set('strict routing', true) - app.set('case sensitive routing', true) - app.set('trust proxy', true) - app.use(function(req, res, next) { - res.setHeader('X-devicehub-unit', 'storage') - next() - }) - app.use(rateLimitConfig) - app.use(cookieSession({ - name: options.ssid, - keys: [options.secret] - })) - app.use(cookieParser()) - app.use(function(req, res, next) { // todo: create a proper auth middleware - req.options = { - secret: options.secret - } - accessTokenAuth(req) - .then(() => { - next() - }) - .catch(err => { - if (options.authUrl) { - res.status(303) - res.setHeader('Location', options.authUrl) - } - else { - res.status(err.status || 500) - } - res.json({message: err.message}) - }) - }) - app.use(bodyParser.json()) - app.use(validator()) - app.use(route) - storage.on('timeout', function(id) { - log.info('Cleaning up inactive resource "%s"', id) - }) - route.post('/s/download/:plugin', function(req, res) { - requtil.validate(req, function() { - req.checkBody('url').notEmpty() - }) - .then(function() { - return download(req.body.url, { - dir: options.cacheDir, - jwt: req.internalJwt - }) - }) - .then(function(file) { - return { - id: storage.store(file), - name: file.name - } - }) - .then(function(file) { - var plugin = req.params.plugin - res.status(201) - .json({ - success: true, - resource: { - date: new Date(), - plugin: plugin, - id: file.id, - name: file.name, - href: util.format('/s/%s/%s%s', plugin, file.id, file.name ? util.format('/%s', path.basename(file.name)) : '') - } - }) - }) - .catch(requtil.ValidationError, function(err) { - res.status(400) - .json({ - success: false, - error: 'ValidationError', - validationErrors: err.errors - }) - }) - .catch(function(err) { - log.error('Error storing resource', err.stack) - res.status(500) - .json({ - success: false, - error: 'ServerError' - }) - }) - }) - route.post('/s/upload/:plugin', function(req, res) { - var form = new formidable.IncomingForm({ - maxFileSize: options.maxFileSize - }) - if (options.saveDir) { - form.uploadDir = options.saveDir - } - form.on('fileBegin', function(name, file) { - if (/\.aab$/.test(file.name)) { - file.isAab = true - } - var md5 = crypto.createHash('md5') - file.name = md5.update(file.name).digest('hex') - }) - Promise.promisify(form.parse, form)(req) - .spread(function(fields, files) { - return Object.keys(files).map(function(field) { - var file = files[field] - log.info('Uploaded "%s" to "%s"', file.name, file.path) - return { - field: field, - id: storage.store(file), - name: file.name, - path: file.path, - isAab: file.isAab - } - }) - }) - .then(function(storedFiles) { - return Promise.all(storedFiles.map(function(file) { - return bundletool({ - bundletoolPath: options.bundletoolPath, - keystore: options.keystore, - file: file - }) - })).then(function(storedFiles) { - res.status(201) - .json({ - success: true, - resources: (function() { - var mapped = Object.create(null) - storedFiles.forEach(function(file) { - var plugin = req.params.plugin - mapped[file.field] = { - date: new Date(), - plugin: plugin, - id: file.id, - name: file.name, - href: util.format('/s/%s/%s%s', plugin, file.id, file.name ? - util.format('/%s', path.basename(file.name)) : - '') - } - }) - return mapped - })() - }) - }) - }) - .catch(function(err) { - log.error('Error storing resource', err.stack) - res.status(500) - .json({ - success: false, - error: 'ServerError' - }) - }) - }) - route.get('/s/blob/:id/:name', function(req, res) { - var file = storage.retrieve(req.params.id) - if (file) { - if (typeof req.query.download !== 'undefined') { - res.set('Content-Disposition', 'attachment; filename="' + path.basename(file.name) + '"') - } - res.set('Content-Type', file.type) - res.sendFile(file.path) - } - else { - res.sendStatus(404) - } - }) - server.listen(options.port) - log.info('Listening on port %d', options.port) -}) diff --git a/lib/units/storage/temp.ts b/lib/units/storage/temp.ts new file mode 100644 index 0000000000..126fd437fb --- /dev/null +++ b/lib/units/storage/temp.ts @@ -0,0 +1,255 @@ +import http from 'http' +import util from 'util' +import path from 'path' +import crypto from 'crypto' +import express from 'express' +import validator from 'express-validator' +import bodyParser from 'body-parser' +import cookieParser from 'cookie-parser' +import logger from '../../util/logger.js' +import Storage from '../../util/storage.js' +import * as requtil from '../../util/requtil.js' +import download from '../../util/download.js' +import bundletool from '../../util/bundletool.js' +import rateLimitConfig from '../ratelimit/index.js' +import { accessTokenAuth } from '../api/helpers/securityHandlers.js' +import cookieSession from 'cookie-session' +import db from '../../db/index.js' + +// @ts-ignore +import formidable from 'formidable' + +interface TempOptions { + ssid: string + secret: string + authUrl?: string + port: number + cacheDir: string + saveDir?: string + maxFileSize: number + bundletoolPath: string + keystore: { + ksPath: string + ksPass: string + ksKeyAlias: string + ksKeyPass: string + ksKeyalg: string + ksKeysize: string + ksDname: string + ksValidity: string + } +} + +interface StoredFile { + field: string + id: string + name: string + path: string + isAab?: boolean +} + +interface ResourceResponse { + date: Date + plugin: string + id: string + name: string + href: string +} + +export default async function (options: TempOptions) { + await db.connect() + + const log = logger.createLogger('storage:temp') + const app = express() + const server = http.createServer(app) + const storage = new Storage() + const route = express.Router() + + app.set('strict routing', true) + app.set('case sensitive routing', true) + app.set('trust proxy', true) + + app.use((req, res, next) => { + res.setHeader('X-devicehub-unit', 'storage') + next() + }) + + app.use(rateLimitConfig) + app.use(cookieSession({ + name: options.ssid, + keys: [options.secret] + })) + app.use(cookieParser()) + + app.use(async (req: any, res, next) => { + req.options = { + secret: options.secret + } + try { + await accessTokenAuth(req) + next() + } catch (err: any) { + if (options.authUrl) { + res.status(303) + res.setHeader('Location', options.authUrl) + } else { + res.status(err.status || 500) + } + res.json({ message: err.message }) + } + }) + + app.use(bodyParser.json()) + app.use(validator()) + app.use(route) + + storage.on('timeout', (id: string) => { + log.info('Cleaning up inactive resource %s', id) + }) + + route.post('/s/download/:plugin', async (req: any, res) => { + try { + await requtil.validate(req, () => { + req.checkBody('url').notEmpty() + }) + + const file = await download(req.body.url, { + dir: options.cacheDir, + jwt: req.internalJwt + }) + + const storedFile = { + id: storage.store(file), + name: file.name + } + + const plugin = req.params.plugin + res.status(201).json({ + success: true, + resource: { + date: new Date(), + plugin: plugin, + id: storedFile.id, + name: storedFile.name, + href: util.format( + '/s/%s/%s%s', + plugin, + storedFile.id, + storedFile.name ? util.format('/%s', path.basename(storedFile.name)) : '' + ) + } + }) + } catch (err: any) { + if (err instanceof requtil.ValidationError) { + res.status(400).json({ + success: false, + error: 'ValidationError', + validationErrors: err.errors + }) + return + } + + log.error('Error storing resource %s', err.stack) + res.status(500).json({ + success: false, + error: 'ServerError' + }) + } + }) + + route.post('/s/upload/:plugin', async (req, res) => { + try { + const form = new formidable.IncomingForm({ + maxFileSize: options.maxFileSize + } as any) + + if (options.saveDir) { + form.uploadDir = options.saveDir + } + + form.on('fileBegin', (name: string, file: any) => { + if (/\.aab$/.test(file.name)) { + file.isAab = true + } + const md5 = crypto.createHash('md5') + file.name = md5.update(file.name).digest('hex') + }) + + const { fields, files } = await new Promise<{ fields: any; files: any }>((resolve, reject) => { + form.parse(req, (err: any, fields: any, files: any) => { + if (err) { + reject(err) + } else { + resolve({ fields, files }) + } + }) + }) + + const storedFiles: StoredFile[] = Object.keys(files).map((field) => { + const file = files[field] + log.info('Uploaded %s to %s', file.name, file.path) + return { + field: field, + id: storage.store(file), + name: file.name, + path: file.path, + isAab: file.isAab + } + }) + + const processedFiles = await Promise.all( + storedFiles.map((file) => + bundletool({ + bundletoolPath: options.bundletoolPath, + keystore: options.keystore, + file: file + }) + ) + ) + + const mapped: Record = Object.create(null) + processedFiles.forEach((file: StoredFile) => { + const plugin = req.params.plugin + mapped[file.field] = { + date: new Date(), + plugin: plugin, + id: file.id, + name: file.name, + href: util.format( + '/s/%s/%s%s', + plugin, + file.id, + file.name ? util.format('/%s', path.basename(file.name)) : '' + ) + } + }) + + res.status(201).json({ + success: true, + resources: mapped + }) + } catch (err: any) { + log.error('Error storing resource %s', err.stack) + res.status(500).json({ + success: false, + error: 'ServerError' + }) + } + }) + + route.get('/s/blob/:id/:name', (req, res) => { + const file = storage.retrieve(req.params.id) + if (file) { + if (typeof req.query.download !== 'undefined') { + res.set('Content-Disposition', 'attachment; filename="' + path.basename(file.name) + '"') + } + res.set('Content-Type', file.type) + res.sendFile(file.path) + } else { + res.sendStatus(404) + } + }) + + server.listen(options.port) + log.info('Listening on port %s', options.port) +} diff --git a/package-lock.json b/package-lock.json index bb8bed333a..340fd282f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@protobuf-ts/plugin": "^2.11.1", "@sentry/node": "^8.34.0", "@types/chrome-remote-interface": "^0.31.14", + "@types/cookie-parser": "^1.4.10", "@u4/adbkit": "^5.1.7", "appium-sdb": "^1.0.1-beta.1", "basic-auth": "1.1.0", @@ -119,6 +120,8 @@ "@eslint/js": "^9.33.0", "@types/bluebird": "^3.5.42", "@types/chalk": "^0.4.31", + "@types/cookie-session": "^2.0.49", + "@types/csurf": "^1.11.5", "@types/eventemitter3": "^1.2.0", "@types/express": "^5.0.1", "@types/http-proxy": "^1.17.16", @@ -4303,6 +4306,26 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "license": "MIT" }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cookie-session": { + "version": "2.0.49", + "resolved": "https://registry.npmjs.org/@types/cookie-session/-/cookie-session-2.0.49.tgz", + "integrity": "sha512-4E/bBjlqLhU5l4iGPR+NkVJH593hpNsT4dC3DJDr+ODm6Qpe13kZQVkezRIb+TYDXaBMemS3yLQ+0leba3jlkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/keygrip": "*" + } + }, "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -4312,6 +4335,16 @@ "@types/node": "*" } }, + "node_modules/@types/csurf": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.5.tgz", + "integrity": "sha512-5rw87+5YGixyL2W8wblSUl5DSZi5YOlXE6Awwn2ofLvqKr/1LruKffrQipeJKUX44VaxKj8m5es3vfhltJTOoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -4399,6 +4432,13 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", diff --git a/package.json b/package.json index 6ed1237c03..70748fb03d 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@protobuf-ts/plugin": "^2.11.1", "@sentry/node": "^8.34.0", "@types/chrome-remote-interface": "^0.31.14", + "@types/cookie-parser": "^1.4.10", "@u4/adbkit": "^5.1.7", "appium-sdb": "^1.0.1-beta.1", "basic-auth": "1.1.0", @@ -137,6 +138,8 @@ "@eslint/js": "^9.33.0", "@types/bluebird": "^3.5.42", "@types/chalk": "^0.4.31", + "@types/cookie-session": "^2.0.49", + "@types/csurf": "^1.11.5", "@types/eventemitter3": "^1.2.0", "@types/express": "^5.0.1", "@types/http-proxy": "^1.17.16", From 709295215a1569191c6eef328782ab761ec0cd96 Mon Sep 17 00:00:00 2001 From: "e.khalilov" Date: Tue, 20 Jan 2026 15:06:11 +0300 Subject: [PATCH 2/2] ts fix --- lib/units/ios-provider/index.ts | 2 +- lib/util/ProcessManager.ts | 2 +- tsconfig.node.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/units/ios-provider/index.ts b/lib/units/ios-provider/index.ts index d45471099f..c16bc40ace 100644 --- a/lib/units/ios-provider/index.ts +++ b/lib/units/ios-provider/index.ts @@ -87,7 +87,7 @@ export default async (options: Options): Promise => { let espTimer: NodeJS.Timeout const espObserver = async() => { // Listen for iMouseDevices - const newDevices = await Esp32Touch.listPorts() + const newDevices = await Esp32Touch.listPorts() as PortInfo[] const diffAdd = _.differenceBy(newDevices, curEsp32, 'path') const diffRemove = _.differenceBy(curEsp32, newDevices, 'path') diff --git a/lib/util/ProcessManager.ts b/lib/util/ProcessManager.ts index 48da752425..9b689e807d 100644 --- a/lib/util/ProcessManager.ts +++ b/lib/util/ProcessManager.ts @@ -176,7 +176,7 @@ export class ProcessManager { } } - const handleError = async (err: Error) => { + const handleError = async (err: any) => { this.log.error('Process "%s" error: %s', id, err.message) cleanup() diff --git a/tsconfig.node.json b/tsconfig.node.json index 724f726c45..0a5d47e9ef 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -3,7 +3,8 @@ "compilerOptions": { "allowJs": false, "checkJs": false, - "noImplicitAny": false + "noImplicitAny": false, + "skipLibCheck": true }, "include": ["./lib/**/*.ts"], "exclude": ["./lib/**/*.js"]