1 <?php
2
3 namespace Alo;
4
5 use Alo\Exception\ExtensionException as EE;
6 use Alo\Exception\SFTPException as SE;
7 use Alo\Exception\SFTPException;
8
9 if (!defined('GEN_START')) {
10 http_response_code(404);
11 die();
12 }
13
14 15 16 17 18
19 class SFTP {
20
21 22 23 24 25
26 const SORT_ASC = SCANDIR_SORT_ASCENDING;
27 28 29 30 31
32 const SORT_DESC = SCANDIR_SORT_DESCENDING;
33 34 35 36 37
38 const P_RETRY_COUNT = 101;
39 40 41 42 43
44 const P_RETRY_TIME = 102;
45 46 47 48 49
50 protected $url;
51 52 53 54 55
56 protected $user;
57 58 59 60 61
62 protected $pubkey;
63 64 65 66 67
68 protected $privkey;
69 70 71 72 73
74 protected $pw;
75 76 77 78 79
80 protected $dir;
81 82 83 84 85
86 protected $connection;
87 88 89 90 91
92 protected $sftp;
93 94 95 96 97
98 protected $local_dir;
99 100 101 102 103
104 protected $retry_count_max;
105
106 107 108 109 110
111 protected $retry_time;
112
113 114 115 116 117 118 119 120
121 function __construct($params = []) {
122 if (!function_exists('ssh2_connect')) {
123 throw new EE('SSH2 extension not loaded', EE::E_EXT_NOT_LOADED);
124 } else {
125 $this->dir = DIR_INDEX;
126
127 $this->retry_count_max = (int)\get($params[self::P_RETRY_COUNT]);
128 $this->retry_time = (int)\get($params[self::P_RETRY_TIME]) ? $params[self::P_RETRY_TIME] : 3;
129
130 \Log::debug('SSH2 class initialised');
131 }
132 }
133
134 135 136 137 138 139 140
141 function retryCount($int = -1) {
142 if ($int === -1) {
143 return $this->retry_count_max;
144 } elseif (is_numeric($int) && $int >= 0) {
145 $this->retry_count_max = (int)$int;
146 \Log::debug('Retry count set to ' . $int);
147
148 return true;
149 } else {
150 return false;
151 }
152 }
153
154 155 156 157 158 159 160
161 function retryTime($int = -1) {
162 if ($int === -1) {
163 return $this->retry_time;
164 } elseif (is_numeric($int) && $int > 0) {
165 $this->retry_time = (int)$int;
166 \Log::debug('Retry time set to ' . $int);
167
168 return true;
169 } else {
170 return false;
171 }
172 }
173
174 175 176 177 178 179 180
181 function loc($dir) {
182 $dir = rtrim($dir, ' ' . DIRECTORY_SEPARATOR);
183 if (!$dir) {
184 $dir = DIRECTORY_SEPARATOR;
185 }
186 $this->local_dir = $dir;
187 \Log::debug('Local dir set to ' . $dir);
188
189 return $this;
190 }
191
192 193 194 195 196 197 198
199 function scandir($sorting_order = self::SORT_ASC) {
200 $this->checkSubsystem();
201
202 $dir = scandir('ssh2.sftp://' . $this->sftp . DIRECTORY_SEPARATOR . $this->dir, $sorting_order);
203 $r = [
204 'dirs' => [],
205 'files' => []
206 ];
207
208 foreach ($dir as $v) {
209
210 if ($v == '.' || $v == '..' || stripos($v, '.') === 0) {
211 continue;
212 } elseif (self::isFile($v)) {
213 $r['files'][] = $v;
214 } else {
215 $r['dirs'][] = $v;
216 }
217 }
218
219 return $r;
220 }
221
222 223 224 225 226 227
228 function checkSubsystem() {
229 if (!$this->sftp) {
230 \Log::debug('SFTP subsystem wasn\'t initialised when a dependant'
231 . ' method was called. Initialising.');
232 $this->connect();
233 }
234
235 return $this;
236 }
237
238 239 240 241 242 243 244 245
246 function connect($attempt = 0) {
247 if (!($this->connection = @ssh2_connect($this->url))) {
248 $msg = 'Failed to initialise SSH2 connection';
249 $attempt++;
250
251 if ($attempt - 1 < $this->retry_count_max) {
252 \Log::error($msg . '. Retrying again' . ' in '
253 . $this->retry_time . ' seconds [' . $attempt
254 . '/' . $this->retry_count_max . ']');
255
256 sleep($this->retry_time);
257
258 return $this->connect($attempt);
259 } else {
260 throw new SE($msg . ' after ' . $attempt . ' attempts', SE::E_CONNECT);
261 }
262 } else {
263 \Log::debug('Initialised SSH2 connection');
264
265 return $this->auth();
266 }
267 }
268
269 270 271 272 273 274 275 276
277 protected function auth($attempt = 0) {
278 if (!ssh2_auth_pubkey_file($this->connection, $this->user, $this->pubkey, $this->privkey, $this->pw)) {
279 $msg = 'Failed to authenticate SSH2 connection';
280 ++$attempt;
281
282 if ($attempt - 1 < $this->retry_count_max) {
283 \Log::error($msg . '. Retrying in ' . $this->retry_time
284 . ' seconds [' . $attempt . '/'
285 . $this->retry_count_max . ']');
286 sleep($this->retry_time);
287
288 return $this->auth($attempt);
289 } else {
290 throw new SE($msg . ' after ' . $attempt . ' retries', SE::E_AUTH);
291 }
292 } else {
293 \Log::debug('SSH2 connection authenticated');
294
295 return $this->ssh2_sftp();
296 }
297 }
298
299 300 301 302 303 304 305 306
307 protected function ssh2_sftp($attempt = 0) {
308 if ($this->sftp = @ssh2_sftp($this->connection)) {
309 \Log::debug('Initialised SFTP subsystem');
310
311 return $this;
312 } else {
313 $msg = 'Failed to initialise SFTP subsystem';
314 ++$attempt;
315
316 if ($attempt - 1 < $this->retry_count_max) {
317 \Log::error($msg . '. Retrying again' . ' in '
318 . $this->retry_time . ' seconds [' . $attempt
319 . '/' . $this->retry_count_max . ']');
320 sleep($this->retry_time);
321
322 return $this->ssh2_sftp($attempt);
323 } else {
324 throw new SE($msg . ' after ' . $attempt . ' attempts', SE::E_SUBSYSTEM);
325 }
326 }
327 }
328
329 330 331 332 333 334 335
336 protected static function isFile($resource) {
337 $pos = stripos($resource, '.');
338
339 return $pos !== false && $pos !== 0;
340 }
341
342 343 344 345 346 347 348 349
350 function downloadFile($file) {
351 \Log::debug('Downloading file ' . $file . ' to ' . $this->local_dir);
352 $fetch = $this->getFileContents($file);
353
354 $local = new File();
355 $local->dir($this->local_dir)->name($file);
356 $local->content($fetch)->write();
357
358 return $this;
359 }
360
361 362 363 364 365 366 367 368
369 function getFileContents($file) {
370 $this->checkSubsystem();
371 $remoteFile = $this->resolvePath($file);
372
373 $file = @file_get_contents('ssh2.sftp://' . $this->sftp . '/' . $remoteFile);
374 if ($file === false) {
375 throw new SE('Failed to fetch file ' . $remoteFile, SE::E_FILE_NOT_FETCHED);
376 } else {
377 return $file;
378 }
379 }
380
381 382 383 384 385 386 387
388 protected function resolvePath($item) {
389 return (stripos($item, DIRECTORY_SEPARATOR) === 0) ? substr($item, 1) : $this->dir . DIRECTORY_SEPARATOR . $item;
390 }
391
392 393 394 395 396 397 398 399
400 function upload($file) {
401 $this->checkSubsystem();
402 $path = $this->local_dir . DIRECTORY_SEPARATOR . $file;
403
404 if (!$content = @file_get_contents($path)) {
405 throw new SE('Local file ' . $path . ' cannot be read', SE::E_LOCAL_FILE_NOT_READ);
406 } else {
407 \Log::debug('Uploading remote file ' . $file);
408
409 return $this->makeFile($file, $content);
410 }
411 }
412
413 414 415 416 417 418 419 420 421
422 function makeFile($file, $content) {
423 $this->checkSubsystem();
424 $remoteFile = $this->resolvePath($file);
425
426 if (!$fp = @fopen('ssh2.sftp://' . $this->sftp . DIRECTORY_SEPARATOR . $remoteFile, File::M_WRITE_TRUNCATE_BEGIN)) {
427 throw new SE('Failed to remotely fopen ' . $remoteFile, SE::E_FILE_CREATE_FAIL);
428 } else {
429 flock($fp, LOCK_EX);
430 fwrite($fp, $content);
431 flock($fp, LOCK_UN);
432 fclose($fp);
433 \Log::debug('Wrote remote file ' . $remoteFile);
434
435 return true;
436 }
437 }
438
439 440 441 442 443 444 445
446 function delete($item) {
447 $this->checkSubsystem();
448 $path = $this->resolvePath($item);
449
450 if (self::isFile($item)) {
451 $success = ssh2_sftp_unlink($this->sftp, $path);
452 } else {
453 $success = ssh2_sftp_rmdir($this->sftp, $path);
454 }
455
456 if ($success) {
457 \Log::debug('Deleted ' . $item);
458
459 return true;
460 } else {
461 \Log::error('Failed to delete ' . $item);
462
463 return false;
464 }
465 }
466
467 468 469 470 471 472
473 function __toString() {
474 return 'User: ' . $this->user . '; PrivKey:' . $this->privkey . '; PubKey: ' . $this->pubkey . '; Password hash: ' . get($this->pw) ? md5($this->pw) : 'NO HASH CONTENT SET';
475 }
476
477 478 479 480 481 482 483 484
485 function url($url = '') {
486 if ($url === '') {
487 return $this->url;
488 } elseif (is_string($url)) {
489 $this->url = $url;
490 \Log::debug('SFTP URL set to ' . $url);
491 } else {
492 throw new SE('Invalid URL', SE::E_URL_INVALID);
493 }
494
495 return $this;
496 }
497
498 499 500 501 502 503 504 505
506 function user($user = '') {
507 if ($user === '') {
508 return $this->user;
509 } elseif (is_scalar($user)) {
510 $this->user = $user;
511 \Log::debug('SFTP user set to ' . $user);
512 } else {
513 throw new SE('Invalid username', SE::E_USER_INVALID);
514 }
515
516 return $this;
517 }
518
519 520 521 522 523 524 525 526
527 function pubkey($pubkey = '') {
528 if ($pubkey === '') {
529 return $this->pubkey;
530 } elseif (is_string($pubkey)) {
531 $this->pubkey = $pubkey;
532 \Log::debug('SFTP pubkey set');
533 } else {
534 throw new SE('$pubkey must be a valid path', SE::E_PATH_INVALID);
535 }
536
537 return $this;
538 }
539
540 541 542 543 544 545 546 547
548 function privkey($privkey = '') {
549 if ($privkey === '') {
550 return $this->privkey;
551 } elseif (is_string($privkey)) {
552 $this->privkey = $privkey;
553 \Log::debug('SFTP privkey set');
554 } else {
555 throw new SE('$privkey must be a valid path', SE::E_PATH_INVALID);
556 }
557
558 return $this;
559 }
560
561 562 563 564 565 566 567 568
569 function pw($pw = '') {
570 if ($pw === '') {
571 return $this->pw;
572 } elseif (is_scalar($pw)) {
573 $this->pw = $pw;
574 \Log::debug('SFTP password set');
575 } else {
576 throw new SE('Invalid password provided', SE::E_PW_INVALID);
577 }
578
579 return $this;
580 }
581
582 583 584 585 586 587 588 589
590 function dir($dir = -1) {
591 if ($dir === -1) {
592 return $this->dir;
593 } elseif (is_scalar($dir)) {
594 $dir = trim($dir, DIRECTORY_SEPARATOR . ' ');
595 if (!$dir || $dir == DIRECTORY_SEPARATOR) {
596 $dir = '.' . DIRECTORY_SEPARATOR;
597 }
598 $this->dir = $dir;
599 \Log::debug('Directory set to ' . $dir);
600 } else {
601 throw new SE('Directory name not scalar', SE::E_NAME_INVALID);
602 }
603
604 return $this;
605 }
606
607 }
608