Skip to content
This repository has been archived by the owner on Oct 25, 2023. It is now read-only.

Commit

Permalink
feat: Allow to enable the access to the system shell via server featu…
Browse files Browse the repository at this point in the history
…res (#38)
  • Loading branch information
mykola-mokhnach committed Dec 18, 2019
1 parent d0ebef7 commit c736fb7
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 23 deletions.
102 changes: 102 additions & 0 deletions lib/commands/execute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { exec } from 'teen_process';
import _ from 'lodash';
import log from '../logger';
import { fs, tempDir } from 'appium-support';
import path from 'path';

const SYSTEM_SHELL_FEATURE = 'system_shell';

const commands = {};

/**
* @typedef {Object} ExecOptions
* @property {?string} interpreter - Full path to the command line interpreter binary.
* The current interpreter (`$SHELL`) or `/bin/bash` is used by default
* @property {?boolean} throwOnFail [false] - Whether to throw an exception if
* the given script has returned non-zero exit code
* @property {?number} timeout [20000] - The default timeout for the script execution.
* @property {?Object} env [process.env] - Additional environment variables for
* the shell script
*/

/**
* @typedef {Object} ExecResult
* @property {string} stdout - Script stdout
* @property {string} stderr - Script stderr
* @property {number} code - Script return code. It will never be other
* than zero if `throwOnFail` option is set to `true`
*/

/**
* Executes the given shell script if the `system_shell`
* server feature is enabled. The command blocks until
* the script finishes its execution or its timeout expires.
*
* @param {!string} script - The actual shell script to execute.
* This should be a valid script snippet.
* @param {?ExecOptions} args
* @return {ExecResult} - The result of the script execution
* @throws {Error} If there was a problem during command execution
*/
commands.execute = async function execute (script, args) {
this.ensureFeatureEnabled(SYSTEM_SHELL_FEATURE);

if (_.isEmpty(script)) {
log.errorAndThrow(`The 'script' argument cannot be empty`);
}

let opts = {};
if (_.isArray(args) && _.isPlainObject(args[0])) {
opts = args[0];
} else if (_.isPlainObject(args)) {
opts = args;
}
const {
interpreter = process.env.SHELL || '/bin/bash',
throwOnFail = false,
timeout,
env,
} = opts;

const tmpRoot = await tempDir.openDir();
try {
const tmpScriptPath = path.resolve(tmpRoot, 'appium.sh');
await fs.writeFile(tmpScriptPath, script, 'utf8');
log.debug(`Executing script using '${interpreter}' shell interpreter:`);
const execOpts = {};
if (_.isInteger(timeout)) {
log.debug(`- Timeout: ${timeout}ms`);
execOpts.timeout = timeout;
}
if (!_.isEmpty(env)) {
log.debug(`- Environment: ${JSON.stringify(env)}`);
execOpts.env = Object.assign({}, process.env, env);
}
log.debug(script);
// TODO: Add some perf measurement here?
const {stdout, stderr} = await exec(interpreter, [tmpScriptPath], execOpts);
return {
stdout,
stderr,
code: 0,
};
} catch (e) {
if (_.has(e, 'code')) {
const {stdout, stderr, code} = e;
// Do not throw if the script return code is not zero
log.debug(`The script has returned non-zero exit code ${code}`);
if (stderr) {
log.debug(`Stderr: ${stderr}`);
}
if (!throwOnFail) {
return {stdout, stderr, code};
}
}
throw e;
} finally {
await fs.rimraf(tmpRoot);
}
};

export { commands };
export default commands;
11 changes: 11 additions & 0 deletions lib/commands/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import executeCmds from './execute';

const commands = {};
Object.assign(
commands,
executeCmds,
// add other command types here
);

export { commands };
export default commands;
16 changes: 7 additions & 9 deletions lib/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { BaseDriver } from 'appium-base-driver';
import { system } from 'appium-support';
import { AppiumForMac, DEFAULT_A4M_HOST} from './appium-for-mac';
import logger from './logger';
import commands from './commands/index';
import _ from 'lodash';

/* eslint-disable no-useless-escape */
const NO_PROXY_LIST = [
['POST', /execute/],
['POST', new RegExp('^/session/[^/]+/execute')],
];

// Appium instantiates this class
Expand All @@ -14,6 +15,10 @@ class MacDriver extends BaseDriver {
super(opts, shouldValidateCaps);
this.jwpProxyActive = false;
this.opts.address = opts.address || DEFAULT_A4M_HOST;

for (const [cmd, fn] of _.toPairs(commands)) {
MacDriver.prototype[cmd] = fn;
}
}

async createSession (...args) {
Expand Down Expand Up @@ -57,13 +62,6 @@ class MacDriver extends BaseDriver {
await super.deleteSession();
}

async execute (script, args) {
if (!this.relaxedSecurityEnabled) {
logger.errorAndThrow(`Appium server must have relaxed security flag set in order to run any shell commands`);
}
return await this.a4mDriver.sendCommand(`/session/${this.sessionId}/execute`, 'POST', {script, args});
}

proxyActive () {
// we always have an active proxy to the AppiumForMac server
return true;
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@
"dependencies": {
"@babel/runtime": "^7.0.0",
"appium-base-driver": "^5.0.0",
"appium-support": "^2.6.0",
"appium-support": "^2.36.0",
"asyncbox": "^2.3.1",
"bluebird": "^3.5.1",
"lodash": "^4.17.4",
"source-map-support": "^0.5.5",
"teen_process": "^1.7.0",
"teen_process": "^1.15.0",
"yargs": "^15.0.1"
},
"scripts": {
Expand Down
23 changes: 23 additions & 0 deletions test/e2e/driver-e2e-specs.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,27 @@ describe('Driver', function () {
let button = await driver.elementByXPath("/AXApplication[@AXTitle='Calculator']/AXWindow[0]/AXGroup[1]/AXButton[@AXDescription='nine']");
await button.click();
});

describe('#execute', function () {
it('should be called with exit code zero', async function () {
driver.relaxedSecurityEnabled = true;
const {stdout, code} = await driver.execute('echo hello');
stdout.trim().should.eql('hello');
code.should.eql(0);
});

it('should be called with exit code non zero', async function () {
driver.relaxedSecurityEnabled = true;
const {stdout, code} = await driver.execute('echo hello; exit 1');
stdout.trim().should.eql('hello');
code.should.eql(1);
});

it('should be called with exit code non zero and throw', async function () {
driver.relaxedSecurityEnabled = true;
await driver.execute('echo hello; exit 1', {
throwOnFail: true,
}).should.eventually.be.rejected;
});
});
});
12 changes: 0 additions & 12 deletions test/unit/driver-specs.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// transpile:mocha

import MacDriver from '../..';
import AppiumForMac from '../../lib/appium-for-mac';

import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
Expand Down Expand Up @@ -77,17 +76,6 @@ describe('driver.js', function () {
});

describe('#execute', function () {
it('should be called', async function () {
const stbA4mDriver = new AppiumForMac();
driver.relaxedSecurityEnabled = true;
driver.a4mDriver = stbA4mDriver;
sinon.mock(stbA4mDriver).expects('sendCommand')
.withExactArgs('/session/abc/execute', 'POST', {script: 'script', args: 'args'})
.once()
.returns('');
await driver.execute('script', 'args').should.eventually.exist;
});

it('should raise when relaxed security is off', async function () {
driver.relaxedSecurityEnabled = false;
await driver.execute('script', 'args').should.eventually.be.rejected;
Expand Down

0 comments on commit c736fb7

Please sign in to comment.