// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import * as path from "path"; import { OutputChannelLogger } from "../log/OutputChannelLogger"; import { AdbHelper } from "./adb"; const allowedAppNameRegex = /^[\w.-]+$/; const appNotApplicationRegex = /not an application/; const appNotDebuggableRegex = /debuggable/; const operationNotPermittedRegex = /not permitted/; const deviceTmpDir = "/data/local/tmp/"; // The code is borrowed from https://github.com/facebook/flipper/blob/master/desktop/app/src/utils/androidContainerUtility.tsx, // https://github.com/facebook/flipper/blob/master/desktop/app/src/utils/androidContainerUtilityInternal.tsx enum RunAsErrorCode { NotAnApp = 1, NotDebuggable = 2, } class RunAsError extends Error { code: RunAsErrorCode; constructor(code: RunAsErrorCode, message?: string) { super(message); this.code = code; Object.setPrototypeOf(this, new.target.prototype); } } /** * @preserve * Start region: the code is borrowed from https://github.com/facebook/flipper/blob/v0.79.1/desktop/app/src/utils/androidContainerUtility.tsx#L19-L46 * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ export function push( adbHelper: AdbHelper, deviceId: string, app: string, filepath: string, contents: string, logger?: OutputChannelLogger, ): Promise { return validateAppName(app).then(validApp => validateFilePath(filepath).then(validFilepath => validateFileContent(contents).then(validContent => _push(adbHelper, deviceId, validApp, validFilepath, validContent, logger), ), ), ); } export function pull( adbHelper: AdbHelper, deviceId: string, app: string, path: string, logger?: OutputChannelLogger, ): Promise { return validateAppName(app).then(validApp => validateFilePath(path).then(validPath => _pull(adbHelper, deviceId, validApp, validPath, logger), ), ); } /** * @preserve * End region: https://github.com/facebook/flipper/blob/v0.79.1/desktop/app/src/utils/androidContainerUtility.tsx#L19-L46 */ export async function pushFile( adbHelper: AdbHelper, deviceId: string, app: string, destFilepath: string, sourceFilepath: string, logger?: OutputChannelLogger, ): Promise { const validApp = await validateAppName(app); const validFilepath = await validateFilePath(destFilepath); return _pushFile(adbHelper, deviceId, validApp, validFilepath, sourceFilepath, logger); } async function _pushFile( adbHelper: AdbHelper, deviceId: string, app: string, destFilepath: string, sourceFilepath: string, logger?: OutputChannelLogger, ): Promise { const destFileName = path.basename(destFilepath); const tmpFilePath = deviceTmpDir + destFileName; try { const pushRes = await adbHelper.executeQuery(deviceId, [ "push", sourceFilepath, tmpFilePath, ]); logger?.debug(pushRes); const command = `cp "${tmpFilePath}" "${destFilepath}" && chmod 644 "${destFilepath}"`; const appCommandRes = await executeCommandAsApp(adbHelper, deviceId, app, command); logger?.debug(appCommandRes); } finally { await adbHelper.executeShellCommand(deviceId, `rm ${tmpFilePath}`); } } /** * @preserve * Start region: the code is borrowed from https://github.com/facebook/flipper/blob/v0.79.1/desktop/app/src/utils/androidContainerUtilityInternal.tsx * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ function validateAppName(app: string): Promise { if (app.match(allowedAppNameRegex)) { return Promise.resolve(app); } return Promise.reject(new Error(`Disallowed run-as user: ${app}`)); } function validateFilePath(filePath: string): Promise { if (!/^[A-Za-z0-9._\/-]+$/.test(filePath)) { return Promise.reject(new Error(`Disallowed filepath characters: ${filePath}`)); } if (filePath.includes("..")) { return Promise.reject(new Error(`Path traversal not allowed: ${filePath}`)); } return Promise.resolve(filePath); } function validateFileContent(content: string): Promise { if (!content.match(/"/)) { return Promise.resolve(content); } return Promise.reject(new Error(`Disallowed escaping file content: ${content}`)); } function _push( adbHelper: AdbHelper, deviceId: string, app: string, filename: string, contents: string, logger?: OutputChannelLogger, ): Promise { const command = `echo \\"${contents}\\" > "${filename}" && chmod 644 "${filename}"`; return executeCommandAsApp(adbHelper, deviceId, app, command) .then(res => { logger?.debug(res); }) .catch(error => { if (error instanceof RunAsError) { // Fall back to running the command directly. This will work if adb is running as root. return executeCommandWithSu(adbHelper, deviceId, app, command) .then(() => undefined) .catch(e => { logger?.debug(e.toString()); throw error; }); } throw error; }); } function _pull( adbHelper: AdbHelper, deviceId: string, app: string, path: string, logger?: OutputChannelLogger, ): Promise { const command = `cat "${path}"`; return executeCommandAsApp(adbHelper, deviceId, app, command).catch(error => { if (error instanceof RunAsError) { // Fall back to running the command directly. This will work if adb is running as root. return executeCommandWithSu(adbHelper, deviceId, app, command).catch(e => { // Throw the original error. logger?.debug(e.toString()); throw error; }); } throw error; }); } // Keep this method private since it relies on pre-validated arguments function executeCommandAsApp( adbHelper: AdbHelper, deviceId: string, app: string, command: string, ): Promise { return _executeCommandWithRunner(adbHelper, deviceId, app, command, `run-as '${app}'`); } function executeCommandWithSu( adbHelper: AdbHelper, deviceId: string, app: string, command: string, ): Promise { return _executeCommandWithRunner(adbHelper, deviceId, app, command, "su"); } function _executeCommandWithRunner( adbHelper: AdbHelper, deviceId: string, app: string, command: string, runner: string, ): Promise { return adbHelper.executeShellCommand(deviceId, `echo '${command}' | ${runner}`).then(output => { if (output.match(appNotApplicationRegex)) { throw new RunAsError( RunAsErrorCode.NotAnApp, `Android package ${app} is not an application. To use it with Flipper, either run adb as root or add an tag to AndroidManifest.xml`, ); } if (output.match(appNotDebuggableRegex)) { throw new RunAsError( RunAsErrorCode.NotDebuggable, `Android app ${app} is not debuggable. To use it with Flipper, add android:debuggable="true" to the application section of AndroidManifest.xml`, ); } if (output.toLowerCase().match(operationNotPermittedRegex)) { throw new Error( `Your android device (${deviceId}) does not support the adb shell run-as command. We're tracking this at https://github.com/facebook/flipper/issues/92`, ); } return output; }); } /** * @preserve * End region: https://github.com/facebook/flipper/blob/v0.79.1/desktop/app/src/utils/androidContainerUtilityInternal.tsx */