215 lines
6.5 KiB
PHP
215 lines
6.5 KiB
PHP
<?php
|
|
/**
|
|
* @author Lukas Reschke <lukas@owncloud.com>
|
|
* @author Morris Jobke <hey@morrisjobke.de>
|
|
* @author Ross Nicoll <jrn@jrn.me.uk>
|
|
*
|
|
* @copyright Copyright (c) 2015, ownCloud, Inc.
|
|
* @license AGPL-3.0
|
|
*
|
|
* This code is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License, version 3,
|
|
* as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License, version 3,
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
*
|
|
*/
|
|
namespace OC\Files\Storage;
|
|
|
|
/**
|
|
* Uses phpseclib's Net_SFTP class and the Net_SFTP_Stream stream wrapper to
|
|
* provide access to SFTP servers.
|
|
*/
|
|
class SFTP_Key extends \OC\Files\Storage\SFTP {
|
|
private $publicKey;
|
|
private $privateKey;
|
|
|
|
public function __construct($params) {
|
|
parent::__construct($params);
|
|
$this->publicKey = $params['public_key'];
|
|
$this->privateKey = $params['private_key'];
|
|
}
|
|
|
|
/**
|
|
* Returns the connection.
|
|
*
|
|
* @return \Net_SFTP connected client instance
|
|
* @throws \Exception when the connection failed
|
|
*/
|
|
public function getConnection() {
|
|
if (!is_null($this->client)) {
|
|
return $this->client;
|
|
}
|
|
|
|
$hostKeys = $this->readHostKeys();
|
|
$this->client = new \Net_SFTP($this->getHost());
|
|
|
|
// The SSH Host Key MUST be verified before login().
|
|
$currentHostKey = $this->client->getServerPublicHostKey();
|
|
if (array_key_exists($this->getHost(), $hostKeys)) {
|
|
if ($hostKeys[$this->getHost()] !== $currentHostKey) {
|
|
throw new \Exception('Host public key does not match known key');
|
|
}
|
|
} else {
|
|
$hostKeys[$this->getHost()] = $currentHostKey;
|
|
$this->writeHostKeys($hostKeys);
|
|
}
|
|
|
|
$key = $this->getPrivateKey();
|
|
if (is_null($key)) {
|
|
throw new \Exception('Secret key could not be loaded');
|
|
}
|
|
if (!$this->client->login($this->getUser(), $key)) {
|
|
throw new \Exception('Login failed');
|
|
}
|
|
return $this->client;
|
|
}
|
|
|
|
/**
|
|
* Returns the private key to be used for authentication to the remote server.
|
|
*
|
|
* @return \Crypt_RSA instance or null in case of a failure to load the key.
|
|
*/
|
|
private function getPrivateKey() {
|
|
$key = new \Crypt_RSA();
|
|
$key->setPassword(\OC::$server->getConfig()->getSystemValue('secret', ''));
|
|
if (!$key->loadKey($this->privateKey)) {
|
|
// Should this exception rather than return null?
|
|
return null;
|
|
}
|
|
return $key;
|
|
}
|
|
|
|
/**
|
|
* Throws an exception if the provided host name/address is invalid (cannot be resolved
|
|
* and is not an IPv4 address).
|
|
*
|
|
* @return true; never returns in case of a problem, this return value is used just to
|
|
* make unit tests happy.
|
|
*/
|
|
public function assertHostAddressValid($hostname) {
|
|
// TODO: Should handle IPv6 addresses too
|
|
if (!preg_match('/^\d+\.\d+\.\d+\.\d+$/', $hostname) && gethostbyname($hostname) === $hostname) {
|
|
// Hostname is not an IPv4 address and cannot be resolved via DNS
|
|
throw new \InvalidArgumentException('Cannot resolve hostname.');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Throws an exception if the provided port number is invalid (cannot be resolved
|
|
* and is not an IPv4 address).
|
|
*
|
|
* @return true; never returns in case of a problem, this return value is used just to
|
|
* make unit tests happy.
|
|
*/
|
|
public function assertPortNumberValid($port) {
|
|
if (!preg_match('/^\d+$/', $port)) {
|
|
throw new \InvalidArgumentException('Port number must be a number.');
|
|
}
|
|
if ($port < 0 || $port > 65535) {
|
|
throw new \InvalidArgumentException('Port number must be between 0 and 65535 inclusive.');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Replaces anything that's not an alphanumeric character or "." in a hostname
|
|
* with "_", to make it safe for use as part of a file name.
|
|
*/
|
|
protected function sanitizeHostName($name) {
|
|
return preg_replace('/[^\d\w\._]/', '_', $name);
|
|
}
|
|
|
|
/**
|
|
* Replaces anything that's not an alphanumeric character or "_" in a username
|
|
* with "_", to make it safe for use as part of a file name.
|
|
*/
|
|
protected function sanitizeUserName($name) {
|
|
return preg_replace('/[^\d\w_]/', '_', $name);
|
|
}
|
|
|
|
public function test() {
|
|
|
|
// FIXME: Use as expression in empty once PHP 5.4 support is dropped
|
|
$host = $this->getHost();
|
|
if (empty($host)) {
|
|
\OC::$server->getLogger()->warning('Hostname has not been specified');
|
|
return false;
|
|
}
|
|
// FIXME: Use as expression in empty once PHP 5.4 support is dropped
|
|
$user = $this->getUser();
|
|
if (empty($user)) {
|
|
\OC::$server->getLogger()->warning('Username has not been specified');
|
|
return false;
|
|
}
|
|
if (!isset($this->privateKey)) {
|
|
\OC::$server->getLogger()->warning('Private key was missing from the request');
|
|
return false;
|
|
}
|
|
|
|
// Sanity check the host
|
|
$hostParts = explode(':', $this->getHost());
|
|
try {
|
|
if (count($hostParts) == 1) {
|
|
$hostname = $hostParts[0];
|
|
$this->assertHostAddressValid($hostname);
|
|
} else if (count($hostParts) == 2) {
|
|
$hostname = $hostParts[0];
|
|
$this->assertHostAddressValid($hostname);
|
|
$this->assertPortNumberValid($hostParts[1]);
|
|
} else {
|
|
throw new \Exception('Host connection string is invalid.');
|
|
}
|
|
} catch(\Exception $e) {
|
|
\OC::$server->getLogger()->warning($e->getMessage());
|
|
return false;
|
|
}
|
|
|
|
// Validate the key
|
|
$key = $this->getPrivateKey();
|
|
if (is_null($key)) {
|
|
\OC::$server->getLogger()->warning('Secret key could not be loaded');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
if ($this->getConnection()->nlist() === false) {
|
|
return false;
|
|
}
|
|
} catch(\Exception $e) {
|
|
// We should be throwing a more specific error, so we're not just catching
|
|
// Exception here
|
|
\OC::$server->getLogger()->warning($e->getMessage());
|
|
return false;
|
|
}
|
|
|
|
// Save the key somewhere it can easily be extracted later
|
|
if (\OC::$server->getUserSession()->getUser()) {
|
|
$view = new \OC\Files\View('/'.\OC::$server->getUserSession()->getUser()->getUId().'/files_external/sftp_keys');
|
|
if (!$view->is_dir('')) {
|
|
if (!$view->mkdir('')) {
|
|
\OC::$server->getLogger()->warning('Could not create secret key directory.');
|
|
return false;
|
|
}
|
|
}
|
|
$key_filename = $this->sanitizeUserName($this->getUser()).'@'.$this->sanitizeHostName($hostname).'.pub';
|
|
$key_file = $view->fopen($key_filename, "w");
|
|
if ($key_file) {
|
|
fwrite($key_file, $this->publicKey);
|
|
fclose($key_file);
|
|
} else {
|
|
\OC::$server->getLogger()->warning('Could not write secret key file.');
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|