PublishSSH   B
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 358
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 0
loc 358
rs 8.2608
c 0
b 0
f 0
wmc 40
lcom 1
cbo 7

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A handle() 0 17 1
A checkForSSHClientOrInstall() 0 9 2
A checkForSSHKeysOrCreate() 0 9 2
A postKeyToLaravelForgeServer() 0 16 2
A abortCommandExecution() 0 4 1
A getUniqueKeyNameFromEmail() 0 4 2
A obtainServersInfo() 0 5 1
A obtainServers() 0 9 2
B obtainServer() 0 28 6
A endPointAPIURL() 0 5 1
A installSSHKeyOnServer() 0 20 3
A appendSSHConfig() 0 23 3
A ip_address() 0 11 3
A validateIpAddress() 0 11 3
A testSSHConnection() 0 9 2
A createSSHKeys() 0 6 2
A getEmail() 0 4 2
A installSshClient() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like PublishSSH often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PublishSSH, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Acacha\ForgePublish\Commands;
4
5
use Acacha\ForgePublish\Commands\Traits\ChecksEnv;
6
use Acacha\ForgePublish\Commands\Traits\ChecksSSHConnection;
7
use Acacha\ForgePublish\Commands\Traits\PossibleEmails;
8
use Acacha\ForgePublish\Commands\Traits\DiesIfEnvVariableIsnotInstalled;
9
use GuzzleHttp\Client;
10
use Illuminate\Console\Command;
11
use Illuminate\Support\Facades\File;
12
13
/**
14
 * Class PublishSSH.
15
 *
16
 * @package Acacha\ForgePublish\Commands
17
 */
18
class PublishSSH extends Command
19
{
20
    use PossibleEmails, ChecksSSHConnection, ChecksEnv, DiesIfEnvVariableIsnotInstalled;
21
22
    /**
23
     * SSH_ID_RSA_PRIV
24
     */
25
    const SSH_ID_RSA_PRIV = '/.ssh/id_rsa';
26
27
    /**
28
     * SSH_ID_RSA_PUB
29
     */
30
    const SSH_ID_RSA_PUB = '/.ssh/id_rsa.pub';
31
32
    /**
33
     * USR_BIN_SSH
34
     */
35
    const USR_BIN_SSH = '/usr/bin/ssh';
36
37
    /**
38
     * Servers.
39
     *
40
     * @var array
41
     */
42
    protected $servers;
43
44
    /**
45
     * Server.
46
     *
47
     * @var array
48
     */
49
    protected $server;
50
51
    /**
52
     * Server name.
53
     *
54
     * @var array
55
     */
56
    protected $server_name;
57
58
    /**
59
     * Server names.
60
     *
61
     * @var array
62
     */
63
    protected $server_names;
64
65
    /**
66
     *API endpint URL
67
     *
68
     * @var String
69
     */
70
    protected $url;
71
72
    /**
73
     * The name and signature of the console command.
74
     *
75
     * @var string
76
     */
77
    protected $signature = 'publish:ssh {email?} {server_name?} {ip?}';
78
79
    /**
80
     * The console command description.
81
     *
82
     * @var string
83
     */
84
    protected $description = 'Add ssh configuration and publish SSH keys to Laravel Forge server';
85
86
    /**
87
     * Guzzle http client.
88
     *
89
     * @var Client
90
     */
91
    protected $http;
92
93
    /**
94
     * Create a new command instance.
95
     *
96
     */
97
    public function __construct(Client $http)
98
    {
99
        parent::__construct();
100
        $this->http = $http;
101
    }
102
103
    /**
104
     * Execute the console command.
105
     *
106
     */
107
    public function handle()
108
    {
109
        $this->info("Configuring SSH...");
110
111
        $this->checkForSSHClientOrInstall();
112
113
        $this->checkForSSHKeysOrCreate();
114
115
        $this->obtainServersInfo();
116
117
        $this->appendSSHConfig();
118
119
        $this->abortCommandExecution();
120
        $this->installSSHKeyOnServer();
121
122
        $this->testSSHConnection();
123
    }
124
125
    /**
126
     * Check for SSH client or install it if missing.
127
     */
128
    protected function checkForSSHClientOrInstall()
129
    {
130
        if (! File::exists(self::USR_BIN_SSH)) {
131
            $this->info('No SSH client found on your system (' . self::USR_BIN_SSH .')!');
132
            $this->installSshClient();
133
        } else {
134
            $this->info('SSH client found in your system (' . self::USR_BIN_SSH .')...');
135
        }
136
    }
137
138
    /**
139
     * Check for SSH client or install it if missing.
140
     */
141
    protected function checkForSSHKeysOrCreate()
0 ignored issues
show
Coding Style introduced by
checkForSSHKeysOrCreate uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
142
    {
143
        if (File::exists($_SERVER['HOME'] . self::SSH_ID_RSA_PRIV)) {
144
            $this->info('SSH keys found on your system (~' . self::SSH_ID_RSA_PRIV .')...');
145
        } else {
146
            $this->info('No SSH keys found on your system (~' . self::SSH_ID_RSA_PRIV .')!');
147
            $this->createSSHKeys();
148
        }
149
    }
150
151
    protected function obtainServersInfo()
152
    {
153
        $this->obtainServers();
154
        $this->obtainServer();
155
    }
156
157
    /**
158
     * Obtain servers.
159
     */
160
    protected function obtainServers()
161
    {
162
        $this->servers = $this->fetchServers();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->fetchServers() of type * is incompatible with the declared type array of property $servers.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
163
        $this->server_names = collect($this->servers)->pluck('name')->toArray();
164
        if (empty($this->server_names)) {
165
            $this->error('No valid servers assigned to user. Skipping SSH key installation on server...');
166
            die();
0 ignored issues
show
Coding Style Compatibility introduced by
The method obtainServers() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
167
        }
168
    }
169
170
    /**
171
     * Obtain server.
172
     */
173
    protected function obtainServer()
174
    {
175
        if (! $this->server) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->server of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
176
            if ($this->argument('server_name')) {
177
                $this->server = $this->getForgeIdServer($this->servers, $this->argument('server_name'));
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getForgeIdServer(...rgument('server_name')) of type * is incompatible with the declared type array of property $server.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
178
                if (!$this->server) {
179
                    $this->error('No server name found on servers assigned to user!');
180
                    die();
0 ignored issues
show
Coding Style Compatibility introduced by
The method obtainServer() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
181
                }
182
                $this->server_name = $this->argument('server_name');
183
            } else {
184
                if (fp_env('ACACHA_FORGE_SERVER')) {
185
                    $this->server = fp_env('ACACHA_FORGE_SERVER');
186
                    if (fp_env('ACACHA_FORGE_SERVER_NAME')) {
187
                        $this->server_name = fp_env('ACACHA_FORGE_SERVER_NAME');
188
                    } else {
189
                        $this->server_name = $this->getForgeName($this->servers, $this->server);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getForgeName($thi...servers, $this->server) of type * is incompatible with the declared type array of property $server_name.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
190
                    }
191
                } else {
192
                    $this->server_name = $this->choice('Forge server?', $this->server_names, 0);
193
                    $this->server = $this->getForgeIdServer($this->servers, $this->server_name);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getForgeIdServer(...rs, $this->server_name) of type * is incompatible with the declared type array of property $server.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
194
                }
195
            }
196
        }
197
198
        $this->checkServer($this->server);
199
        $this->checkServerName($this->server_name);
200
    }
201
202
    /**
203
     * Get ENDPOINT API URL.
204
     *
205
     * @return string
206
     */
207
    protected function endPointAPIURL()
208
    {
209
        $uri = str_replace('{forgeserver}', $this->server, config('forge-publish.post_ssh_keys_uri'));
210
        return config('forge-publish.url') . $uri;
211
    }
212
213
    /**
214
     * Install ssh key on server.
215
     */
216
    protected function installSSHKeyOnServer()
217
    {
218
        $this->info('Adding SSH key to Laravel Forge server');
219
        // We cannot use ssh-copy-id -i ~/.ssh/id_rsa.pub [email protected] because SSH acces via user/password is not enabled on Laravel Forge
220
        // We need to use the Laravel Forge API to add a key
221
222
        $this->url = $this->endPointAPIURL();
223
224
        $response = $this->postKeyToLaravelForgeServer($keyName = $this->getUniqueKeyNameFromEmail());
225
226
        $result = json_decode($contents = $response->getBody()->getContents());
227
228
        if (! isset($result->status)) {
229
            $this->error("An error has been succeded: $contents");
230
            die();
0 ignored issues
show
Coding Style Compatibility introduced by
The method installSSHKeyOnServer() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
231
        }
232
        if ($result->status == 'installed') {
233
            $this->info("The SSH Key ($keyName) has been correctly installed in Laravel Forge Server " . $this->server_name . ' (' . $this->server . ')');
234
        }
235
    }
236
237
    /**
238
     * Post key to Laravel Forge Server.
239
     */
240
    protected function postKeyToLaravelForgeServer($keyName=null)
0 ignored issues
show
Coding Style introduced by
postKeyToLaravelForgeServer uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
241
    {
242
        $keyName ? $keyName : $this->getUniqueKeyNameFromEmail();
243
        return $this->http->post($this->url,
244
            [
245
                'form_params' => [
246
                    'name' => $keyName,
247
                    'key' => file_get_contents($_SERVER['HOME'] . self::SSH_ID_RSA_PUB)
248
                ],
249
                'headers' => [
250
                    'X-Requested-With' => 'XMLHttpRequest',
251
                    'Authorization' => 'Bearer ' . fp_env('ACACHA_FORGE_ACCESS_TOKEN')
252
                ]
253
            ]
254
        );
255
    }
256
257
    /**
258
     * Abort command execution?
259
     */
260
    protected function abortCommandExecution()
261
    {
262
        $this->dieIfEnvVarIsNotInstalled('ACACHA_FORGE_ACCESS_TOKEN');
263
    }
264
265
    /**
266
     * Get unique key name from email.
267
     */
268
    protected function getUniqueKeyNameFromEmail()
269
    {
270
        return str_slug($this->argument('email') ? $this->argument('email') : $this->getEmail()) . '_' . str_random(10);
271
    }
272
273
    /**
274
     * Append ssh config.
275
     */
276
    protected function appendSSHConfig()
277
    {
278
        $ssh_config_file = $this->sshConfigFile();
279
280
        $hostname = $this->hostNameForConfigFile();
281
282
        $host_string = "Host " . $hostname;
283
284
        if (strpos(file_get_contents($ssh_config_file), $host_string) !== false) {
285
            $this->info("SSH config for host: $host_string already exists");
286
            return;
287
        }
288
289
        $this->info("Adding server config to SSH config file $ssh_config_file");
290
        if (! File::exists($ssh_config_file)) {
291
            touch($ssh_config_file);
292
        }
293
        $ip_address = $this->ip_address();
294
        $config_string = "\n$host_string\n  Hostname $ip_address \n  User forge\n  IdentityFile /home/sergi/.ssh/id_rsa\n  Port 22\n  StrictHostKeyChecking no\n";
295
        File::append($ssh_config_file, $config_string);
296
297
        $this->info('The following config has been added:' . $config_string);
298
    }
299
300
    /**
301
     * IP address.
302
     *
303
     * @return array|string
304
     */
305
    protected function ip_address()
306
    {
307
        if (fp_env('ACACHA_FORGE_IP_ADDRESS')) {
308
            $ip_address = fp_env('ACACHA_FORGE_IP_ADDRESS');
309
        } else {
310
            $ip_address = $this->argument('ip') ? $this->argument('ip') : $this->ask('IP Address?');
311
        }
312
313
        $this->validateIpAddress($ip_address);
314
        return $ip_address;
315
    }
316
317
    /**
318
     * Validate ip address.
319
     *
320
     * @param $ip_address
321
     */
322
    protected function validateIpAddress($ip_address)
323
    {
324
        if (! filter_var($ip_address, FILTER_VALIDATE_IP)) {
325
            $this->error("$ip_address is not a valid ip address! Exiting!");
326
            die();
0 ignored issues
show
Coding Style Compatibility introduced by
The method validateIpAddress() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
327
        }
328
        if (! $this->checkIp($ip_address, $this->servers)) {
329
            $this->error("$ip_address doesn't match any of your IP addreses Forge Servers");
330
            die();
0 ignored issues
show
Coding Style Compatibility introduced by
The method validateIpAddress() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
331
        }
332
    }
333
334
    /**
335
     * Test SSH connection.
336
     */
337
    protected function testSSHConnection()
338
    {
339
        $this->info('Testing connection...');
340
        if ($this->checkSSHConnection()) {
341
            $this->info('Connection tested ok!');
342
        } else {
343
            $this->error('Error connnecting to server!');
344
        }
345
    }
346
347
    /**
348
     * Create SSH Keys.
349
     */
350
    protected function createSSHKeys()
351
    {
352
        $email = $this->argument('email') ? $this->argument('email') : $this->getEmail();
353
        $this->info("Running ssh-keygen -t rsa -b 4096 -C '$email'");
354
        passthru('ssh-keygen -t rsa -b 4096 -C "' . $email . '"');
355
    }
356
357
    /**
358
     * Get email.
359
     *
360
     * @return string
361
     */
362
    protected function getEmail()
363
    {
364
        return array_key_exists(0, $emails = $this->getPossibleEmails()) ? $emails[0] : $this->ask('Email:');
365
    }
366
367
    /**
368
     * Install ssh client.
369
     */
370
    protected function installSshClient()
371
    {
372
        $this->info('Running sudo apt-get install ssh');
373
        passthru('sudo apt-get install openssh-client');
374
    }
375
}
376