Skip to content

Commit

Permalink
Merge pull request #17 from netsells/bugfix/ssm-tunnel
Browse files Browse the repository at this point in the history
Bug fixes
  • Loading branch information
Jakub Gawron authored Mar 2, 2022
2 parents e6fb9f5 + 14cd6ac commit c8b9d4a
Show file tree
Hide file tree
Showing 7 changed files with 980 additions and 815 deletions.
27 changes: 19 additions & 8 deletions app/Commands/AwsAssumeRole.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class AwsAssumeRole extends BaseCommand
*
* @var string
*/
protected $description = 'Handles MFA Login';
protected $description = 'Allows to assume a role in selected AWS account.';

public function configure()
{
Expand All @@ -42,13 +42,23 @@ public function handle()

$accountId = $this->menu("Choose an account to connect to...", array_combine($accounts->pluck('id')->all(), $accounts->pluck('name')->all()))->open();

if (!$accountId) {
$this->info('No account selected, exiting.');
return 0;
}

$account = $accounts->firstWhere('id', $accountId);
$accountName = $account['name'];

$roles = collect($this->helpers->aws()->s3()->getJsonFile('netsells-security-meta', 'roles.json')['roles'])->pluck('name')->all();

$role = $this->menu("Choose a role to assume...", array_combine($roles, $roles))->open();

if (!$role) {
$this->info('No role selected, exiting.');
return 0;
}

$callerArn = $this->helpers->aws()->iam()->getCallerArn();
$sessionUser = 'unknown.user';

Expand All @@ -65,7 +75,7 @@ public function handle()
$mfaDevice = $this->askForMfaDevice($this);

if (!$mfaDevice) {
$this->info("Access was denied assuming role {$role}. We tried to initiate an MFA session but you have no devices available for user {$sessionUser}.");
$this->error("Access was denied assuming role {$role}. We tried to initiate an MFA session but you have no devices available for user {$sessionUser}.");
return 1;
}

Expand All @@ -80,13 +90,14 @@ public function handle()
}
}

$assumePrompt = "{$sessionUser}:{$accountName}";

$envVars['AWS_S3_ENV'] = $account['s3env'];

$this->info("Now opening a session following you ({$sessionUser}) assuming the role {$role} on {$accountName} ({$accountId}) . Type `exit` to leave this shell.");
Process::fromShellCommandline("BASH_SILENCE_DEPRECATION_WARNING=1 PS1='\e[32mnscli\e[34m({$assumePrompt})$\e[39m ' bash")
->setEnv($envVars)

(new Process([config('app.shell')]))
->setEnv(array_merge($envVars, [
'AWS_S3_ENV' => $account['s3env'],
'BASH_SILENCE_DEPRECATION_WARNING' => '1',
'PS1' => "\e[32mnscli\e[34m({$sessionUser}:{$accountName})$\e[39m ",
]))
->setTty(Process::isTtySupported())
->setIdleTimeout(null)
->setTimeout(null)
Expand Down
9 changes: 7 additions & 2 deletions app/Commands/AwsMfaLogin.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Commands;

use Symfony\Component\Process\Process;

class AwsMfaLogin extends BaseCommand
{
/**
Expand Down Expand Up @@ -50,8 +51,12 @@ public function handle()
$envVars = $this->helpers->aws()->iam()->authenticateWithMfaDevice($mfaDevice, $code);

$this->info("Now opening a session following your MFA authentication. Type `exit` to leave this shell.");
Process::fromShellCommandline("BASH_SILENCE_DEPRECATION_WARNING=1 PS1='\e[32mnscli\e[34m(mfa-authd)$\e[39m ' bash")
->setEnv($envVars)

(new Process([config('app.shell')]))
->setEnv(array_merge($envVars, [
'BASH_SILENCE_DEPRECATION_WARNING' => '1',
'PS1' => "\e[32mnscli\e[34m(mfa-authd)$\e[39m ",
]))
->setTty(Process::isTtySupported())
->setIdleTimeout(null)
->setTimeout(null)
Expand Down
36 changes: 16 additions & 20 deletions app/Commands/AwsSsmConnect.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function configure()
*/
public function handle()
{
$requiredBinaries = ['aws', 'ssh'];
$requiredBinaries = ['aws', 'ssh', 'ssh-keygen'];

if ($this->helpers->checks()->checkAndReportMissingBinaries($requiredBinaries)) {
return 1;
Expand All @@ -65,24 +65,11 @@ public function handle()
$rebuildOptions = $this->appendResolvedArgument($rebuildOptions, 'aws-profile');
$rebuildOptions = $this->appendResolvedArgument($rebuildOptions, 'aws-region');

$key = $this->generateTempSshKey();
$command = $this->generateRemoteCommand($username, $key);

$this->info("Sending a temporary SSH key to the server...", OutputInterface::VERBOSITY_VERBOSE);
if (!$this->helpers->aws()->ssm()->sendRemoteCommand($instanceId, $command)) {
$this->error('Failed to send SSH key to server');
return 1;
}

$sessionCommand = $this->helpers->aws()->ssm()->startSessionProcess($instanceId);
$sessionCommandString = implode(' ', $sessionCommand->getArguments());

$options = [
'-o', 'IdentityFile ~/.ssh/netsells-cli-ssm-ssh-tmp',
'-o', 'IdentitiesOnly yes',
'-o', 'GSSAPIAuthentication no',
'-o', 'PasswordAuthentication no',
'-o', "ProxyCommand {$sessionCommandString}",
];

if ($this->option('tunnel')) {
Expand All @@ -99,7 +86,22 @@ public function handle()
$rebuildOptions = $this->appendResolvedArgument($rebuildOptions, 'tunnel-remote-server', $tunnelRemoteServer);
$rebuildOptions = $this->appendResolvedArgument($rebuildOptions, 'tunnel-remote-port', $tunnelRemotePort);
$rebuildOptions = $this->appendResolvedArgument($rebuildOptions, 'tunnel-local-port', $tunnelLocalPort);
}

$command = $this->generateRemoteCommand($username, $this->generateTempSshKey());

$this->info("Sending a temporary SSH key to the server...", OutputInterface::VERBOSITY_VERBOSE);
if (!$this->helpers->aws()->ssm()->sendRemoteCommand($instanceId, $command)) {
$this->error('Failed to send SSH key to server');
return 1;
}

$sessionCommand = $this->helpers->aws()->ssm()->startSessionProcess($instanceId);
$sessionCommandString = implode(' ', $sessionCommand->getArguments());

array_unshift($options, '-o', "ProxyCommand {$sessionCommandString}");

if ($this->option('tunnel')) {
$this->sendReRunHelper($rebuildOptions);

$this->info("Establishing an SSH tunnel connection with {$instanceId}, this may take a few seconds...");
Expand Down Expand Up @@ -191,12 +193,6 @@ protected function askForInstanceId()

private function generateTempSshKey()
{
$requiredBinaries = ['aws', 'ssh', 'ssh-keygen'];

if ($this->helpers->checks()->checkAndReportMissingBinaries($requiredBinaries)) {
return 1;
}

$sshDir = $_SERVER['HOME'] . '/.ssh/';
$keyName = $sshDir . $this->tempKeyName;
$pubKeyName = "{$keyName}.pub";
Expand Down
54 changes: 27 additions & 27 deletions app/UpdateStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,26 @@

namespace App;

use Humbug;
use Humbug\SelfUpdate\Exception\HttpRequestException;
use Humbug\SelfUpdate\Updater;
use LaravelZero\Framework\Components\Updater\Strategy\StrategyInterface;

class UpdateStrategy implements StrategyInterface
{
/**
* @var string
*/
private $localVersion;
private const GITHUB_LATEST_RELEASE_URL = 'https://github.com/netsells/cli/releases/latest/download/netsells.phar';
private const GITHUB_SPECIFIC_RELEASE_PATTERN = '/download\/([^\/]+)\/netsells\.phar/';

/**
* @var string
*/
private $remoteVersion;
private $localVersion;

/**
* @var string
*/
private $remoteUrl;

/**
* @var string
*/
private $pharName;

/**
* @var string
*/
Expand All @@ -42,8 +37,9 @@ public function download(Updater $updater)
{
/** Switch remote request errors to HttpRequestExceptions */
set_error_handler(array($updater, 'throwHttpRequestException'));
$result = humbug_get_contents($this->remoteUrl);
$result = Humbug\get_contents($this->remoteUrl);
restore_error_handler();

if (false === $result) {
throw new HttpRequestException(sprintf(
'Request to URL failed: %s', $this->remoteUrl
Expand All @@ -62,29 +58,33 @@ public function download(Updater $updater)
public function getCurrentRemoteVersion(Updater $updater)
{
/** Switch remote request errors to HttpRequestExceptions */
set_error_handler(array($updater, 'throwHttpRequestException'));
$versionUrl = 'https://netsells-cli.now.sh/version';
$version = json_decode(humbug_get_contents($versionUrl), true);
set_error_handler([$updater, 'throwHttpRequestException']);

// we want to make a HEAD request
// just to get the location header from github to the specific latest version
$context = stream_context_create([
'http' => [
'method' => 'HEAD',
'follow_location' => 0,
],
]);

$headers = get_headers(self::GITHUB_LATEST_RELEASE_URL, true, $context);

restore_error_handler();

if (null === $version || json_last_error() !== JSON_ERROR_NONE) {
throw new JsonParsingException(
'Error parsing JSON package data'
. (function_exists('json_last_error_msg') ? ': ' . json_last_error_msg() : '')
);
if (!is_array($headers) || !isset($headers['Location'])) {
return false;
}

$this->remoteUrl = $headers['Location'];

$this->remoteVersion = $version['version'];

/**
* Setup remote URL if there's an actual version to download
*/
if (!empty($this->remoteVersion)) {
$this->remoteUrl = 'https://netsells-cli.now.sh/download/cli';
if (preg_match(self::GITHUB_SPECIFIC_RELEASE_PATTERN, $this->remoteUrl, $matches) !== 1 || count($matches) !== 2 || empty($matches[1])) {
return false;
}

return $this->remoteVersion;
// contains the matched version, e.g. v2.3.1
return $matches[1];
}

/**
Expand Down
Loading

0 comments on commit c8b9d4a

Please sign in to comment.