├── .gitignore ├── LICENSE.TXT ├── bin ├── yoke └── yokephp ├── composer.json ├── readme.md └── src ├── Console └── Commands │ ├── AddCommand.php │ ├── BaseCommand.php │ ├── ConnectCommand.php │ ├── DeleteCommand.php │ ├── EditCommand.php │ └── ServersCommand.php ├── Servers ├── Exceptions │ └── NotFoundException.php ├── Manager.php └── Server.php └── Storage ├── Encryptor.php └── Manager.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 - Diego Hernandes 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /bin/yoke: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Desired command. 4 | Y_COMMAND=${1} 5 | # Path of phpyoke script. 6 | Y_PATH=$(which yokephp) 7 | # Path of active PHP. 8 | Y_PHP_PATH=$(which php) 9 | 10 | # If the current command is to connect. 11 | if [ "${Y_COMMAND}" == "connect" ] || [ "${Y_COMMAND}" == "c" ]; then 12 | # save the execution return into a variable for later parsing. 13 | Y_RETURN=$(${Y_PHP_PATH} "${Y_PATH}" "${Y_COMMAND}" "${@: 2}") 14 | else 15 | # If the command is other than connect, pass through. 16 | ${Y_PHP_PATH} "${Y_PATH}" "${Y_COMMAND}" "${@: 2}" 17 | # Define a empty Y_RETURN 18 | Y_RETURN="" 19 | fi 20 | 21 | # If the return starts with ssh. 22 | if [[ ${Y_RETURN} =~ ^ssh.* ]]; then 23 | # Execute the returned line into the terminal (connect). 24 | eval "${Y_RETURN}" 25 | fi 26 | 27 | # If the return starts with Password. 28 | if [[ ${Y_RETURN} =~ ^Password.* ]]; then 29 | # Break the return into the password helper and command variables. 30 | Y_PASSWORD_HELPER=$(echo -e "${Y_RETURN}" | awk 'NR==1') 31 | Y_PASSWORD_COMMAND=$(echo -e "${Y_RETURN}" | awk 'NR==2') 32 | 33 | # Display the password 34 | echo "${Y_PASSWORD_HELPER}" 35 | # Connect 36 | eval "${Y_PASSWORD_COMMAND}" 37 | fi 38 | -------------------------------------------------------------------------------- /bin/yokephp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | setName('Yoke: SSH Connection Manager'); 23 | // Add Yoke commands. 24 | $yoke->add(new AddCommand()); 25 | $yoke->add(new ServersCommand()); 26 | $yoke->add(new ConnectCommand()); 27 | $yoke->add(new DeleteCommand()); 28 | $yoke->add(new EditCommand()); 29 | // Run the application. 30 | $yoke->run(); 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yokessh/yoke", 3 | "description": "Yoke: SSH Connection Manager/Wallet", 4 | "keywords": [ 5 | "ssh", 6 | "manager", 7 | "wallet" 8 | ], 9 | "type": "project", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Diego Hernandes", 14 | "email": "diego@hernandev.com" 15 | }, 16 | { 17 | "name": "Lucas Mezêncio", 18 | "email": "eu@lucasmezencio.com" 19 | } 20 | ], 21 | "require": { 22 | "php": ">=8.1", 23 | "ext-json": "*", 24 | "ext-openssl": "*", 25 | "symfony/console": "^6", 26 | "symfony/yaml": "^6", 27 | "symfony/process": "^6" 28 | }, 29 | "require-dev": { 30 | "roave/security-advisories": "dev-latest", 31 | "symfony/var-dumper": "^6" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Yoke\\": "src/" 36 | } 37 | }, 38 | "minimum-stability": "stable", 39 | "bin": [ 40 | "bin/yokephp", 41 | "bin/yoke" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Yoke: SSH Connection Manager 2 | 3 | Yoke is a PHP based **SSH connection manager**. Sometimes storing servers hosts, usernames, ports and passwords can be tricky, SSH Key authentication makes it easier for us, but it doesn't solve the problem of remembering all the other information. 4 | Also, sometimes we face ourselves with more than one private key to authenticate with (like multiple accounts on AWS). 5 | 6 | Yoke aims to be a single repository for server managements to allow you to fastly connect to your servers just by remembering it's alias, like. 7 | 8 | ```shell 9 | yoke connect myserver 10 | ``` 11 | 12 | With security in mind, all information about your servers is encrypted using **AES 256**. 13 | 14 | **NOTICE** The encryption key is also stored into your computer, Yoke encryption only makes it harder for users to identify and decrypt the information. But just like SSH private keys, it does not protect against people getting access to your filesystem. 15 | 16 | ### Installation 17 | 18 | In order to use Yoke, you need PHP 8+ installed, with openssl extension enables (default on most installs) 19 | 20 | The installation process is based on the global composer packages, so you need to have a working composer install with the correct binary path settings. [Read this tutorial ](https://akrabat.com/global-installation-of-php-tools-with-composer/) 21 | 22 | If you have the requirements, install Yoke by running: 23 | 24 | ```shell 25 | composer global require yokessh/yoke 26 | ``` 27 | 28 | This is all you need to do! Time for usage instructions. 29 | 30 | ### Usage 31 | 32 | Using Yoke is really simple and straightforward. 33 | 34 | #### Adding a Server Connection 35 | 36 | In order to store a new connection, just run the command 37 | 38 | ```shell 39 | yoke add [alias] 40 | ``` 41 | 42 | You will then be presented with a few questions: 43 | 44 | ``` 45 | Registering a new Server Configuration! 46 | 47 | Server connection alias (server1): sample-server 48 | 49 | 👤 Server username (none): sample-user 50 | 51 | 🖥️ Server hostname or IP Address (192.168.0.1): server.sampleapp.com 52 | 53 | 🚪 Server Port (22): 6262 54 | 55 | 🔐 Authentication Method:[system|key|password] (system): key 56 | 57 | 🔑 Private Key (~/.ssh/id_rsa): 58 | 59 | Server registered successfully! 🥳 60 | ``` 61 | 62 | #### Connecting 63 | 64 | As we have this connection in place, we can establish a connection, anytime we want just by running a simple command: 65 | 66 | ```shell 67 | yoke connect sample-server 68 | ``` 69 | 70 | In case it's a server with a password, you can optionally ask to show the password when connecting: 71 | 72 | ```shell 73 | yoke connect sample-server --password 74 | ``` 75 | 76 | Easy right? 77 | 78 | #### Listing connections 79 | 80 | Forgot a server alias? Don't worry, you can just run: 81 | 82 | ```shell 83 | yoke servers 84 | ``` 85 | 86 | To see a list of stored connections, like this one 87 | 88 | ```markdown 89 | +---------------+----------------------+-------------+------+--------------+ 90 | | Name | Host | Username | Port | Auth. Method | 91 | +---------------+----------------------+-------------+------+--------------+ 92 | | server-a | a.sampleapp.comm | admin | 22 | key | 93 | | server-b | b.sampleapp.com | root | 2222 | system | 94 | | server-c | c.sampleapp.com | root | 22 | password | 95 | +---------------+----------------------+-------------+------+--------------+ 96 | ``` 97 | 98 | #### Removing a connection 99 | Don't need a stored connection anymore? 100 | 101 | Just run 102 | 103 | ```shell 104 | yoke delete alias 105 | ``` 106 | 107 | Confirm the deletion and it's done!. 108 | 109 | 110 | #### Final Notes: 111 | There are 3 different allows authentication types: 112 | 113 | - `key` - uses a specified private key to establish the connection 114 | - `system` - Do not specify a private key to connect, it lets ssh try to connection with current user's key 115 | - `password` - SSH does not allow passing plain password as a parameter, Yoke will just show the password on screen, so you can copy and paste it. **Password authentication is highly unrecommended**. 116 | -------------------------------------------------------------------------------- /src/Console/Commands/AddCommand.php: -------------------------------------------------------------------------------- 1 | arguments = [ 21 | new InputArgument('alias', InputArgument::OPTIONAL, 'Connection alias'), 22 | ]; 23 | 24 | parent::__construct(); 25 | } 26 | 27 | /** 28 | * Execute the command. 29 | * 30 | * @param InputInterface $input 31 | * 32 | * @return int 33 | * 34 | * @throws Exception 35 | */ 36 | protected function fire(InputInterface $input): int 37 | { 38 | // Greetings. 39 | $this->info('Registering a new Server Configuration!'); 40 | 41 | $serverData = []; 42 | 43 | // Read initial connection data. 44 | if (!$serverData['alias'] = $input->getArgument('alias')) { 45 | $serverData['alias'] = $this->ask('Server connection alias (server1):', 'server1'); 46 | } 47 | 48 | $serverData['user'] = $this->ask('👤 Server username (none):'); 49 | $serverData['host'] = $this->ask('🖥️ Server hostname or IP Address (192.168.0.1):', '192.168.0.1'); 50 | $serverData['port'] = $this->ask('🚪 Server Port (22):', 22); 51 | $serverData['authenticationMethod'] = $this->ask( 52 | '🔐 Authentication Method:[system|key|password] (system):', 53 | 'system' 54 | ); 55 | 56 | if ('key' === $serverData['authenticationMethod']) { 57 | // Ask for private key if key was selected as authentication method. 58 | $serverData['privateKey'] = $this->ask('🔑 Private Key (~/.ssh/id_rsa):', "{$_SERVER['HOME']}/.ssh/id_rsa"); 59 | } 60 | 61 | if ($this->askConfirmation( 62 | "🗄️ Is there any SSH option you would like to add? (eg: -o 'PubkeyAcceptedKeyTypes +ssh-rsa')" 63 | )) { 64 | $serverData['sshOption'] = $this->ask('Option:'); 65 | } 66 | 67 | if ('password' === $serverData['authenticationMethod']) { 68 | // Ask for password if password as selected as authentication method. 69 | $serverData['password'] = $this->ask('🔒 Password:'); 70 | } 71 | 72 | // Register the server connection data into servers manager. 73 | $this->manager->createServer($serverData); 74 | 75 | // If the server was indeed created, congratulate the user. 76 | if ($this->manager->serverExists($serverData['alias'])) { 77 | $this->comment('Server registered successfully! 🥳'); 78 | } 79 | 80 | return self::SUCCESS; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Console/Commands/BaseCommand.php: -------------------------------------------------------------------------------- 1 | input = $input; 41 | $this->output = $output; 42 | // Assign the QuestionHelper instance. 43 | $this->questionHelper = $this->getHelper('question'); 44 | // Created and assign a new Servers Manager instance. 45 | $this->manager = new Manager(); 46 | 47 | // Call the child command fire() method. 48 | return $this->fire($input); 49 | } 50 | 51 | /** 52 | * Initialize the command for the console Application. 53 | */ 54 | protected function configure(): void 55 | { 56 | // Configure the command name. 57 | $this->setName($this->name); 58 | // Configure the command description. 59 | $this->setDescription($this->description); 60 | 61 | $alias = strtolower($this->name[0]); 62 | 63 | // Configure single letter alias for the command. 64 | $this->setAliases([$alias]); 65 | $this->setDefinition( 66 | new InputDefinition([ 67 | ...$this->arguments, 68 | ...$this->options, 69 | ]) 70 | ); 71 | } 72 | 73 | /** 74 | * Abstract fire method to be implemented on child commands. 75 | * 76 | * @param InputInterface $input 77 | * 78 | * @return int Success or Failure 79 | */ 80 | abstract protected function fire(InputInterface $input): int; 81 | 82 | /** 83 | * Abstracts the question process into a single method. 84 | * Some options are defined by convention, like formatting. 85 | * 86 | * @param string $question The question being asked. 87 | * @param null $default Default value in case the user does not provide an answer. 88 | * 89 | * @return string The user input or the default value. 90 | */ 91 | protected function ask(string $question, $default = null): string 92 | { 93 | // Creates a new question instance, using the convention formatting. 94 | $askQuestion = new Question($this->format($question, 'question'), $default); 95 | 96 | // Do ask que question created and return its answer value. 97 | return $this->questionHelper->ask($this->input, $this->output, $askQuestion); 98 | } 99 | 100 | /** 101 | * Asks a Confirmation (Yes/No) Question. 102 | * 103 | * Inputs like Y, Yes will make this method return true. 104 | * Any other input will return false. 105 | * 106 | * @param string $question The question to be confirmed. 107 | * 108 | * @return bool Confirmed or Not. 109 | */ 110 | protected function askConfirmation(string $question): bool 111 | { 112 | // Creates a new confirmation instance using convention formatting. 113 | $confirmQuestion = new ConfirmationQuestion($this->format("{$question} (y/N)", 'question'), false); 114 | 115 | // Ask and return the input. 116 | return $this->questionHelper->ask($this->input, $this->output, $confirmQuestion); 117 | } 118 | 119 | /** 120 | * Format a given string into a colored output format. 121 | * 122 | * @param string $text The string to be formatted. 123 | * @param string $type Desired coloring type. 124 | * 125 | * @return string The formatted string. 126 | */ 127 | protected function format(string $text, string $type = 'info'): string 128 | { 129 | return "\n<{$type}>{$text} "; 130 | } 131 | 132 | /** 133 | * Write a string into the console output. 134 | * 135 | * @param string $text The string to be displayed. 136 | * @param string $format The coloring format. 137 | */ 138 | protected function writeln(string $text, string $format = 'info'): void 139 | { 140 | // Uses output handler to write the formatted string. 141 | $this->output->writeln($this->format($text, $format)); 142 | } 143 | 144 | /** 145 | * Write a not formatted string into the console output. 146 | * 147 | * @param string $text The string to be displayed. 148 | */ 149 | protected function writelnPlain(string $text): void 150 | { 151 | // Uses output handler to write the formatted string. 152 | $this->output->writeln($text); 153 | } 154 | 155 | /** 156 | * Write a question formatted string into the console. 157 | * 158 | * @param string $text The string to be displayed. 159 | */ 160 | protected function question(string $text): void 161 | { 162 | $this->writeln($text, 'question'); 163 | } 164 | 165 | /** 166 | * Write a information formatted string into the console. 167 | * 168 | * @param string $text The string to be displayed. 169 | */ 170 | protected function info(string $text): void 171 | { 172 | $this->writeln($text); 173 | } 174 | 175 | /** 176 | * Write a comment formatted string into the console. 177 | * 178 | * @param string $text The string to be displayed. 179 | */ 180 | protected function comment(string $text): void 181 | { 182 | $this->writeln($text, 'comment'); 183 | } 184 | 185 | /** 186 | * Write a error formatted string into the console. 187 | * 188 | * @param string $text The string to be displayed. 189 | */ 190 | protected function error(string $text): void 191 | { 192 | $this->writeln($text, 'error'); 193 | } 194 | 195 | /** 196 | * Gets the provided value to a given command argument. 197 | * 198 | * @param string $name Argument's name 199 | * 200 | * @return mixed The user provided value. 201 | */ 202 | protected function argument(string $name): mixed 203 | { 204 | return $this->input->getArgument($name); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Console/Commands/ConnectCommand.php: -------------------------------------------------------------------------------- 1 | arguments = [ 20 | new InputArgument('alias', InputArgument::REQUIRED, 'Connection alias'), 21 | ]; 22 | $this->options = [ 23 | new InputOption('password', null, InputOption::VALUE_OPTIONAL, 'Show password'), 24 | new InputOption('user', null, InputOption::VALUE_OPTIONAL, 'Alternative user'), 25 | ]; 26 | 27 | parent::__construct(); 28 | } 29 | 30 | /** 31 | * Execute the command. 32 | * 33 | * @param InputInterface $input 34 | * 35 | * @return int 36 | */ 37 | protected function fire(InputInterface $input): int 38 | { 39 | // Gets the desired connection alias. 40 | $alias = $this->argument('alias'); 41 | // Finds the store server connection using the provided alias. 42 | $server = $this->manager->getServer($alias); 43 | 44 | if (null === $server) { 45 | $this->writeln('Nah!'); 46 | 47 | return self::FAILURE; 48 | } 49 | 50 | if ($user = $input->getOption('user')) { 51 | $server->user = $user; 52 | } 53 | 54 | // Write the console line to be executed on the bash side of 55 | // the string. sometimes it will contain a password 56 | // for usage while authenticating 57 | $this->writelnPlain($server->connectionString($input->getOption('password'))); 58 | 59 | return self::SUCCESS; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Console/Commands/DeleteCommand.php: -------------------------------------------------------------------------------- 1 | arguments = [ 21 | new InputArgument('alias', InputArgument::REQUIRED, 'The connection to be removed.'), 22 | ]; 23 | 24 | parent::__construct(); 25 | } 26 | 27 | /** 28 | * Execute the command. 29 | * 30 | * @param InputInterface $input 31 | * 32 | * @return int 33 | * 34 | * @throws Exception 35 | */ 36 | protected function fire(InputInterface $input): int 37 | { 38 | // Find the server. 39 | $alias = $this->argument('alias'); 40 | 41 | // Ensure server exists. 42 | $this->manager->getServer($alias); 43 | // Greetings. 44 | $this->info('Server connection removal.'); 45 | 46 | // Ask for confirmation. 47 | $confirmed = $this->askConfirmation("Are you sure about deleting the connection {$alias}:"); 48 | 49 | // If confirmed. 50 | if ($confirmed) { 51 | // Delete the connection. 52 | $this->manager->deleteServer($alias); 53 | // And congratulate. 54 | $this->info('Server connection deleted successfully!'); 55 | } 56 | 57 | return self::SUCCESS; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Console/Commands/EditCommand.php: -------------------------------------------------------------------------------- 1 | arguments = [ 22 | new InputArgument('alias', InputArgument::REQUIRED, 'Connection alias'), 23 | ]; 24 | 25 | parent::__construct(); 26 | } 27 | 28 | /** 29 | * Execute the command. 30 | * 31 | * @param InputInterface $input 32 | * 33 | * @return int 34 | * 35 | * @throws \Exception 36 | */ 37 | protected function fire(InputInterface $input): int 38 | { 39 | // Gets the desired connection alias. 40 | $alias = $this->argument('alias'); 41 | // Finds the store server connection using the provided alias. 42 | $server = $this->manager->getServer($alias); 43 | 44 | if (null === $server) { 45 | $this->writeln('Nah!'); 46 | 47 | return self::INVALID; 48 | } 49 | 50 | $oldData = $newData = $server->toArray(); 51 | 52 | $newData['user'] = $this->ask(sprintf('👤 Server username (current: %s):', $server->user), $server->user); 53 | $newData['host'] = $this->ask( 54 | sprintf('🖥️ Server hostname or IP Address (current: %s):', $server->host), 55 | $server->host 56 | ); 57 | $newData['port'] = $this->ask(sprintf('🚪 Server Port (current: %s):', $server->port), $server->port); 58 | $newData['authenticationMethod'] = $this->ask( 59 | sprintf('🔐 Authentication Method [system|key|password]: (current: %s):', $server->authenticationMethod), 60 | $server->authenticationMethod 61 | ); 62 | 63 | if ('key' === $server->authenticationMethod) { 64 | // Ask for private key if key was selected as authentication method. 65 | $newData['privateKey'] = $this->ask("🔑 Private Key (current: {$server->privateKey}):", $server->privateKey); 66 | } 67 | 68 | if ($this->askConfirmation( 69 | "🗄️ Is there any SSH option you would like to add? (eg: -o 'PubkeyAcceptedKeyTypes +ssh-rsa')" 70 | )) { 71 | $newData['sshOption'] = $this->ask("Option (current: {$server->sshOption}):", $server->sshOption); 72 | } 73 | 74 | $table = new Table($this->output); 75 | $table->setHeaders(['Field', 'Old', 'New']); 76 | 77 | $rows = []; 78 | 79 | foreach (array_keys($newData) as $field) { 80 | $rows[] = [$field, $oldData[$field], $newData[$field]]; 81 | } 82 | 83 | $table->setRows($rows); 84 | $table->render(); 85 | 86 | if ($this->askConfirmation('Confirm the changes?')) { 87 | $this->manager->createServer($server->toArray()); 88 | 89 | $this->info('Done! 🥳'); 90 | 91 | return self::SUCCESS; 92 | } 93 | 94 | $this->writeln('Nothing has been changed. 🙃'); 95 | 96 | return self::SUCCESS; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Console/Commands/ServersCommand.php: -------------------------------------------------------------------------------- 1 | manager->getServers(); 31 | 32 | // If there is no servers registered. 33 | // @todo: this shouldn't be an error, just a warning maybe 34 | if (!count($servers)) { 35 | throw new NotFoundException('No servers available.'); 36 | } 37 | 38 | // Render the servers table. 39 | $this->serversTable($servers); 40 | 41 | return self::SUCCESS; 42 | } 43 | 44 | /** 45 | * Renders the servers' table into console. 46 | * 47 | * @param array $servers 48 | */ 49 | protected function serversTable(array $servers): void 50 | { 51 | // New table instance. 52 | $table = new Table($this->output); 53 | // Set table headers. 54 | $table->setHeaders(['Name', 'Host', 'Username', 'Port', 'Auth. Method', 'SSH Option']); 55 | 56 | // Loop on available connections to build the rows. 57 | $rows = []; 58 | 59 | /** @var Server $server */ 60 | foreach ($servers as $server) { 61 | $rows[] = [ 62 | $server->alias, 63 | $this->isIP($server->host) ? "{$server->host}" : $server->host, 64 | $server->user, 65 | "{$server->port}", 66 | $server->authenticationMethod, 67 | $server->sshOption, 68 | ]; 69 | } 70 | 71 | // Set the table rows 72 | $table->setRows($rows); 73 | // Render the table. 74 | $table->render(); 75 | } 76 | 77 | private function isIP(string $host): bool 78 | { 79 | return (bool)filter_var($host, FILTER_VALIDATE_IP); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Servers/Exceptions/NotFoundException.php: -------------------------------------------------------------------------------- 1 | loadServers(); 17 | } 18 | 19 | /** 20 | * Load the stored servers. 21 | * 22 | * @throws \JsonException 23 | */ 24 | protected function loadServers(): void 25 | { 26 | // Get the servers' configuration array from the servers.yml file. 27 | $servers = $this->storageManager->getConfiguration(); 28 | 29 | // Register each server configuration as a Server instance. 30 | foreach ($servers as $serverData) { 31 | $this->registerServer($serverData); 32 | } 33 | } 34 | 35 | /** 36 | * Register a configuration array as a server instance. 37 | * 38 | * @param array $data Server connection information. 39 | */ 40 | public function registerServer(array $data): void 41 | { 42 | // Generate a new server connection instance. 43 | $server = new Server($data); 44 | // Register the server connection into the servers list. 45 | $this->servers[$server->alias] = $server; 46 | } 47 | 48 | /** 49 | * Create a server connection instance with the provided configuration 50 | * data and write the configuration server with the updated information. 51 | * 52 | * @param array $data Server connection information. 53 | * 54 | * @throws Exception 55 | */ 56 | public function createServer(array $data): void 57 | { 58 | // Register a new server connection instance. 59 | $this->registerServer($data); 60 | // Write the configuration server with the updated information 61 | $this->writeServers(); 62 | } 63 | 64 | /** 65 | * Deletes a server connection from instance and storage. 66 | * 67 | * @param string $alias Alias of the server connection to be deleted. 68 | * 69 | * @throws Exception 70 | */ 71 | public function deleteServer(string $alias): void 72 | { 73 | // Find the server. 74 | $server = $this->getServer($alias); 75 | 76 | if ($server) { 77 | // Forget the server from the server connection instances list 78 | unset($this->servers[$server->alias]); 79 | 80 | // Write the updated configuration file 81 | $this->writeServers(); 82 | } 83 | } 84 | 85 | /** 86 | * Write the current server instances into a servers.yml storage file. 87 | * 88 | * @throws Exception 89 | */ 90 | protected function writeServers(): void 91 | { 92 | $servers = []; 93 | 94 | /** 95 | * Parse all current instances into the array representation. 96 | * 97 | * @var Server $server 98 | */ 99 | foreach ($this->servers as $server) { 100 | $servers[] = $server->toArray(); 101 | } 102 | 103 | // Write the servers array into the servers.yml file. 104 | $this->storageManager->writeConfiguration($servers); 105 | } 106 | 107 | /** 108 | * Get a server instance. 109 | * 110 | * @param string $alias Server connection alias. 111 | * 112 | * @return Server|null The Server connection instance. 113 | * 114 | * @throws NotFoundException When the desired alias is not registered. 115 | */ 116 | public function getServer(string $alias): ?Server 117 | { 118 | if ($this->serverExists($alias)) { 119 | return $this->servers[$alias]; 120 | } 121 | 122 | throw new NotFoundException('Server not found.'); 123 | } 124 | 125 | public function getServers(): array 126 | { 127 | ksort($this->servers); 128 | 129 | return $this->servers; 130 | } 131 | 132 | /** 133 | * Is a given connection alias registered? 134 | * 135 | * @param string $alias The given server connection instance alias. 136 | * 137 | * @return bool Registered or not. 138 | */ 139 | public function serverExists(string $alias): bool 140 | { 141 | return array_key_exists($alias, $this->servers); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Servers/Server.php: -------------------------------------------------------------------------------- 1 | $value) { 39 | // Set into its own attribute. 40 | $this->$key = $value; 41 | } 42 | } 43 | 44 | /** 45 | * Magic __set method. 46 | * 47 | * @param string $key Attribute name. 48 | * @param mixed $value Attribute value 49 | */ 50 | public function __set(string $key, mixed $value) 51 | { 52 | // set the value into class attribute. 53 | $this->$key = $value; 54 | } 55 | 56 | /** 57 | * Magic __get method. 58 | * 59 | * @param string $key desired attribute. 60 | * 61 | * @return mixed 62 | */ 63 | public function __get(string $key) 64 | { 65 | return $this->$key; 66 | } 67 | 68 | /** 69 | * @return string The port portion of the shh connection string. 70 | */ 71 | protected function portParameter(): string 72 | { 73 | return $this->port ? "-p{$this->port}" : ''; 74 | } 75 | 76 | /** 77 | * @return string The password helper line. 78 | */ 79 | public function passwordHelper(): string 80 | { 81 | return "Password: {$this->password}"; 82 | } 83 | 84 | /** 85 | * @return string User and hostname portion of the ssh connection string. 86 | */ 87 | protected function userAndHostParameter(): string 88 | { 89 | if ($this->user) { 90 | return "{$this->user}@{$this->host}"; 91 | } 92 | 93 | return $this->host; 94 | } 95 | 96 | /** 97 | * @return string Private key portion of the connection string. 98 | */ 99 | protected function keyParameter(): string 100 | { 101 | if ('key' === $this->authenticationMethod) { 102 | return "-i{$this->privateKey}"; 103 | } 104 | 105 | return ''; 106 | } 107 | 108 | /** 109 | * @param bool|null $showPassword 110 | * 111 | * @return string The final ssh connection string. 112 | */ 113 | public function connectionString(?bool $showPassword = false): string 114 | { 115 | $connectionString = sprintf( 116 | 'ssh %s %s %s %s', 117 | $this->keyParameter(), 118 | $this->portParameter(), 119 | $this->userAndHostParameter(), 120 | $this->sshOption 121 | ); 122 | 123 | if ('password' === $this->authenticationMethod && $showPassword) { 124 | $connectionString = "{$this->passwordHelper()}\n{$connectionString}"; 125 | } 126 | 127 | return $connectionString; 128 | } 129 | 130 | /** 131 | * @return array Array encoded representation of the server connection. 132 | */ 133 | public function toArray(): array 134 | { 135 | $configArray = []; 136 | 137 | if (isset($this->alias)) { 138 | $configArray['alias'] = $this->alias; 139 | } 140 | 141 | if (isset($this->host)) { 142 | $configArray['host'] = $this->host; 143 | } 144 | 145 | if (isset($this->port)) { 146 | $configArray['port'] = $this->port; 147 | } 148 | 149 | if (isset($this->user)) { 150 | $configArray['user'] = $this->user; 151 | } 152 | 153 | if (isset($this->authenticationMethod)) { 154 | $configArray['authenticationMethod'] = $this->authenticationMethod; 155 | } 156 | 157 | if (isset($this->password)) { 158 | $configArray['password'] = $this->password; 159 | } 160 | 161 | if (isset($this->privateKey)) { 162 | $configArray['privateKey'] = $this->privateKey; 163 | } 164 | 165 | if (isset($this->sshOption)) { 166 | $configArray['sshOption'] = $this->sshOption; 167 | } 168 | 169 | return $configArray; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Storage/Encryptor.php: -------------------------------------------------------------------------------- 1 | ivSize); 42 | // Encrypt the value. 43 | $value = openssl_encrypt(serialize($payload), $this->cipher, $this->key, 0, $originalIv); 44 | 45 | // If the value was not encrypted successfully. 46 | if ($value === false) { 47 | // Throw an exception. 48 | throw new RuntimeException('Could not Encrypt the give value.'); 49 | } 50 | 51 | $data = $originalIv . $value; 52 | // Calculate the HMAC. 53 | $mac = hash_hmac('sha256', $data, $this->key); 54 | // Encode IV into encodable format. 55 | $iv = base64_encode($originalIv); 56 | // Encode the IV, Value and HMAC into a JSON Payload that will be stored. 57 | $json = json_encode(compact('iv', 'value', 'mac'), JSON_THROW_ON_ERROR); 58 | 59 | // Check for the encoded json string 60 | if (!is_string($json)) { 61 | throw new RuntimeException('Could not encrypt the given data.'); 62 | } 63 | 64 | // return the encrypted and encoded payload 65 | return base64_encode($json); 66 | } 67 | 68 | /** 69 | * Decrypt a given value. 70 | * 71 | * @param mixed $payload The encrypted payload 72 | * 73 | * @return mixed The Decrypted value. 74 | * 75 | * @throws JsonException 76 | */ 77 | public function decrypt(mixed $payload): mixed 78 | { 79 | // Decode the json payload into an array. 80 | $payload = json_decode(base64_decode($payload), true, 512, JSON_THROW_ON_ERROR); 81 | // Get the IV from the payload. 82 | $iv = base64_decode($payload['iv']); 83 | // Decrypt the value using the key and IV 84 | $decrypted = openssl_decrypt($payload['value'], $this->cipher, $this->key, 0, $iv); 85 | 86 | // If the value was not correctly encrypted 87 | if ($decrypted === false) { 88 | // Throw an exception 89 | throw new RuntimeException('Could not decrypt the given payload.'); 90 | } 91 | 92 | // return the decrypted value. 93 | return unserialize($decrypted); 94 | } 95 | 96 | /** 97 | * Static Key generation method. 98 | * 99 | * @return string A random encryption key. 100 | * 101 | * @throws Exception 102 | */ 103 | public static function generateKey(): string 104 | { 105 | return base64_encode(random_bytes(32)); 106 | } 107 | 108 | /** 109 | * Decrypt a array of values. 110 | * 111 | * @param array $data Encrypted array payload. 112 | * 113 | * @return array Decrypted array. 114 | * 115 | * @throws JsonException 116 | */ 117 | public function decryptArray(array $data): array 118 | { 119 | $decryptedArray = []; 120 | 121 | foreach ($data as $key => $payload) { 122 | $decryptedArray[$key] = $this->decrypt($payload); 123 | } 124 | 125 | return $decryptedArray; 126 | } 127 | 128 | /** 129 | * Encrypt a given array. 130 | * 131 | * @param array $data Array to be encrypted. 132 | * 133 | * @return array Encrypted array. 134 | * 135 | * @throws Exception 136 | */ 137 | public function encryptArray(array $data): array 138 | { 139 | $encryptedArray = []; 140 | 141 | foreach ($data as $key => $value) { 142 | $encryptedArray[$key] = $this->encrypt($value); 143 | } 144 | 145 | return $encryptedArray; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Storage/Manager.php: -------------------------------------------------------------------------------- 1 | encryptor = new Encryptor($this->getOrGenerateKey()); 26 | } 27 | 28 | /** 29 | * Get a existing or create a new encryption key. 30 | * 31 | * This method reads the encryption key of the encryption.key file, if this file 32 | * does not exist, it generates a new key using the encryptor static method 33 | * and then stores it. 34 | * 35 | * WARNING: If the encryption key is changed the stored values will not be able of 36 | * decryption. 37 | * 38 | * @return string The encryption key. 39 | * 40 | * @throws Exception 41 | */ 42 | protected function getOrGenerateKey(): string 43 | { 44 | // Generates a new key is none exists. 45 | if (!$this->fileExists('encryption.key')) { 46 | $this->storeFile('encryption.key', Encryptor::generateKey()); 47 | } 48 | 49 | // Return the encryption key. 50 | return trim($this->getContents('encryption.key')); 51 | } 52 | 53 | /** 54 | * Return a array of values for a given configuration file. 55 | * Defaults to servers.yml (the .yml extension should be omitted). 56 | * 57 | * @param string $type Configuration file name without .yml prefix. 58 | * 59 | * @return array Decrypted configuration array. 60 | * 61 | * @throws JsonException 62 | */ 63 | public function getConfiguration(string $type = 'servers'): array 64 | { 65 | // If the requested configuration file exists. 66 | if ($this->fileExists("{$type}.yml")) { 67 | // Create a new YML parser instance. 68 | $parser = new Parser(); 69 | // Gets the encrypted configuration array from YML. 70 | $encryptedConfiguration = $parser->parse($this->getContents("{$type}.yml")); 71 | 72 | // Decrypt and them return the configuration array. 73 | return $this->encryptor->decryptArray($encryptedConfiguration); 74 | //return $this->encryptor->handleMultidimensionalArray($encryptedConfiguration, 'decrypt'); 75 | } 76 | 77 | // Otherwise, just return an empty array. 78 | return []; 79 | } 80 | 81 | /** 82 | * Write a given array into a encrypted storage file. 83 | * 84 | * @param array $data The data to be stored. 85 | * @param string $type The actual filename without the .yml extension to store the information. 86 | * 87 | * @throws Exception 88 | */ 89 | public function writeConfiguration(array $data, string $type = 'servers'): void 90 | { 91 | // Encrypt the configuration array 92 | //$encryptedArray = $this->encryptor->handleMultidimensionalArray($data, 'encrypt'); 93 | $encryptedArray = $this->encryptor->encryptArray($data); 94 | // Transform the encrypted data into YAML. 95 | $dumper = new Dumper(); 96 | $configuration = $dumper->dump($encryptedArray, 2); 97 | 98 | // Store the encrypted file. 99 | $this->storeFile("{$type}.yml", $configuration); 100 | } 101 | 102 | /** 103 | * @return string The storage base path. 104 | */ 105 | public function basePath(): string 106 | { 107 | return "{$_SERVER['HOME']}/.yoke"; 108 | } 109 | 110 | /** 111 | * Prepare the storage path in case the directory don't already exists. 112 | */ 113 | protected function prepareBasePath(): void 114 | { 115 | if (!is_dir($this->basePath())) { 116 | mkdir($this->basePath()); 117 | } 118 | } 119 | 120 | /** 121 | * Get the full path for a relative filename. 122 | * 123 | * @param null $file 124 | * 125 | * @return string 126 | */ 127 | protected function path($file = null): string 128 | { 129 | // Check if the folder exists, create if it doesn't. 130 | $this->prepareBasePath(); 131 | 132 | // If a file name is provided, return it's full path. 133 | if ($file) { 134 | return "{$this->basePath()}/{$file}"; 135 | } 136 | 137 | // Return the base path otherwise. 138 | return $this->basePath(); 139 | } 140 | 141 | /** 142 | * Check for a file existence. 143 | * 144 | * @param string $file Relative file name. 145 | * 146 | * @return bool Exists or not. 147 | */ 148 | protected function fileExists(string $file): bool 149 | { 150 | return file_exists($this->path($file)); 151 | } 152 | 153 | /** 154 | * Create or replace a given file with the given contents. 155 | * 156 | * @param string $name File name. 157 | * @param string $contents File contents. 158 | */ 159 | protected function storeFile(string $name, string $contents): void 160 | { 161 | file_put_contents($this->path($name), $contents); 162 | } 163 | 164 | /** 165 | * Reads a given file contents. 166 | * 167 | * @param string $name Desired file. 168 | * 169 | * @return string The file contents. 170 | */ 171 | protected function getContents(string $name): string 172 | { 173 | return file_get_contents($this->path($name)); 174 | } 175 | } 176 | --------------------------------------------------------------------------------