Browse Source
Migrate SFTP_Key external storage to new API
Migrate SFTP_Key external storage to new API
The SFTP backend now supports public key authentication alongside password authentication.remotes/origin/db-empty-migrate
10 changed files with 177 additions and 287 deletions
-
13apps/files_external/appinfo/app.php
-
4apps/files_external/appinfo/application.php
-
2apps/files_external/appinfo/routes.php
-
46apps/files_external/js/public_key.js
-
53apps/files_external/js/sftp_key.js
-
65apps/files_external/lib/auth/publickey/rsa.php
-
1apps/files_external/lib/backend/sftp.php
-
48apps/files_external/lib/backend/sftp_key.php
-
17apps/files_external/lib/sftp.php
-
215apps/files_external/lib/sftp_key.php
@ -0,0 +1,46 @@ |
|||
$(document).ready(function() { |
|||
|
|||
OCA.External.Settings.mountConfig.whenSelectAuthMechanism(function($tr, authMechanism, scheme) { |
|||
if (scheme === 'publickey') { |
|||
var config = $tr.find('.configuration'); |
|||
if ($(config).find('[name="public_key_generate"]').length === 0) { |
|||
setupTableRow($tr, config); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
$('#externalStorage').on('click', '[name="public_key_generate"]', function(event) { |
|||
event.preventDefault(); |
|||
var tr = $(this).parent().parent(); |
|||
generateKeys(tr); |
|||
}); |
|||
|
|||
function setupTableRow(tr, config) { |
|||
$(config).append($(document.createElement('input')) |
|||
.addClass('button auth-param') |
|||
.attr('type', 'button') |
|||
.attr('value', t('files_external', 'Generate keys')) |
|||
.attr('name', 'public_key_generate') |
|||
); |
|||
// If there's no private key, build one
|
|||
if (0 === $(config).find('[data-parameter="private_key"]').val().length) { |
|||
generateKeys(tr); |
|||
} |
|||
} |
|||
|
|||
function generateKeys(tr) { |
|||
var config = $(tr).find('.configuration'); |
|||
|
|||
$.post(OC.filePath('files_external', 'ajax', 'public_key.php'), {}, function(result) { |
|||
if (result && result.status === 'success') { |
|||
$(config).find('[data-parameter="public_key"]').val(result.data.public_key); |
|||
$(config).find('[data-parameter="private_key"]').val(result.data.private_key); |
|||
OCA.External.Settings.mountConfig.saveStorageConfig(tr, function() { |
|||
// Nothing to do
|
|||
}); |
|||
} else { |
|||
OC.dialogs.alert(result.data.message, t('files_external', 'Error generating key pair') ); |
|||
} |
|||
}); |
|||
} |
|||
}); |
|||
@ -1,53 +0,0 @@ |
|||
$(document).ready(function() { |
|||
|
|||
$('#externalStorage tbody tr.\\\\OC\\\\Files\\\\Storage\\\\SFTP_Key').each(function() { |
|||
var tr = $(this); |
|||
var config = $(tr).find('.configuration'); |
|||
if ($(config).find('.sftp_key').length === 0) { |
|||
setupTableRow(tr, config); |
|||
} |
|||
}); |
|||
|
|||
// We can't catch the DOM elements being added, but we can pick up when
|
|||
// they receive focus
|
|||
$('#externalStorage').on('focus', 'tbody tr.\\\\OC\\\\Files\\\\Storage\\\\SFTP_Key', function() { |
|||
var tr = $(this); |
|||
var config = $(tr).find('.configuration'); |
|||
|
|||
if ($(config).find('.sftp_key').length === 0) { |
|||
setupTableRow(tr, config); |
|||
} |
|||
}); |
|||
|
|||
$('#externalStorage').on('click', '.sftp_key', function(event) { |
|||
event.preventDefault(); |
|||
var tr = $(this).parent().parent(); |
|||
generateKeys(tr); |
|||
}); |
|||
|
|||
function setupTableRow(tr, config) { |
|||
$(config).append($(document.createElement('input')).addClass('button sftp_key') |
|||
.attr('type', 'button') |
|||
.attr('value', t('files_external', 'Generate keys'))); |
|||
// If there's no private key, build one
|
|||
if (0 === $(config).find('[data-parameter="private_key"]').val().length) { |
|||
generateKeys(tr); |
|||
} |
|||
} |
|||
|
|||
function generateKeys(tr) { |
|||
var config = $(tr).find('.configuration'); |
|||
|
|||
$.post(OC.filePath('files_external', 'ajax', 'sftp_key.php'), {}, function(result) { |
|||
if (result && result.status === 'success') { |
|||
$(config).find('[data-parameter="public_key"]').val(result.data.public_key); |
|||
$(config).find('[data-parameter="private_key"]').val(result.data.private_key); |
|||
OCA.External.mountConfig.saveStorageConfig(tr, function() { |
|||
// Nothing to do
|
|||
}); |
|||
} else { |
|||
OC.dialogs.alert(result.data.message, t('files_external', 'Error generating key pair') ); |
|||
} |
|||
}); |
|||
} |
|||
}); |
|||
@ -0,0 +1,65 @@ |
|||
<?php |
|||
/** |
|||
* @author Robin McCorkell <rmccorkell@owncloud.com> |
|||
* |
|||
* @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 OCA\Files_External\Lib\Auth\PublicKey; |
|||
|
|||
use \OCP\IL10N; |
|||
use \OCA\Files_External\Lib\DefinitionParameter; |
|||
use \OCA\Files_External\Lib\Auth\AuthMechanism; |
|||
use \OCA\Files_External\Lib\StorageConfig; |
|||
use \OCP\IConfig; |
|||
use \phpseclib\Crypt\RSA as RSACrypt; |
|||
|
|||
/** |
|||
* RSA public key authentication |
|||
*/ |
|||
class RSA extends AuthMechanism { |
|||
|
|||
/** @var IConfig */ |
|||
private $config; |
|||
|
|||
public function __construct(IL10N $l, IConfig $config) { |
|||
$this->config = $config; |
|||
|
|||
$this |
|||
->setIdentifier('publickey::rsa') |
|||
->setScheme(self::SCHEME_PUBLICKEY) |
|||
->setText($l->t('RSA public key')) |
|||
->addParameters([ |
|||
(new DefinitionParameter('user', $l->t('Username'))), |
|||
(new DefinitionParameter('public_key', $l->t('Public key'))), |
|||
(new DefinitionParameter('private_key', 'private_key')) |
|||
->setType(DefinitionParameter::VALUE_HIDDEN), |
|||
]) |
|||
->setCustomJs('public_key') |
|||
; |
|||
} |
|||
|
|||
public function manipulateStorageConfig(StorageConfig &$storage) { |
|||
$auth = new RSACrypt(); |
|||
$auth->setPassword($this->config->getSystemValue('secret', '')); |
|||
if (!$auth->loadKey($storage->getBackendOption('private_key'))) { |
|||
throw new \RuntimeException('unable to load private key'); |
|||
} |
|||
$storage->setBackendOption('public_key_auth', $auth); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
<?php |
|||
/** |
|||
* @author Robin McCorkell <rmccorkell@owncloud.com> |
|||
* |
|||
* @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 OCA\Files_External\Lib\Backend; |
|||
|
|||
use \OCP\IL10N; |
|||
use \OCA\Files_External\Lib\Backend\Backend; |
|||
use \OCA\Files_External\Lib\DefinitionParameter; |
|||
use \OCA\Files_External\Lib\Auth\AuthMechanism; |
|||
use \OCA\Files_External\Service\BackendService; |
|||
use \OCA\Files_External\Lib\Auth\PublicKey\RSA; |
|||
|
|||
class SFTP_Key extends Backend { |
|||
|
|||
public function __construct(IL10N $l, RSA $legacyAuth) { |
|||
$this |
|||
->setIdentifier('\OC\Files\Storage\SFTP_Key') |
|||
->setStorageClass('\OC\Files\Storage\SFTP') |
|||
->setText($l->t('SFTP with secret key login [DEPRECATED]')) |
|||
->addParameters([ |
|||
(new DefinitionParameter('host', $l->t('Host'))), |
|||
(new DefinitionParameter('root', $l->t('Remote subfolder'))) |
|||
->setFlag(DefinitionParameter::FLAG_OPTIONAL), |
|||
]) |
|||
->addAuthScheme(AuthMechanism::SCHEME_PUBLICKEY) |
|||
->setLegacyAuthMechanism($legacyAuth) |
|||
; |
|||
} |
|||
|
|||
} |
|||
@ -1,215 +0,0 @@ |
|||
<?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; |
|||
|
|||
use phpseclib\Crypt\RSA; |
|||
|
|||
class SFTP_Key extends \OC\Files\Storage\SFTP { |
|||
private $publicKey; |
|||
private $privateKey; |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function __construct($params) { |
|||
parent::__construct($params); |
|||
$this->publicKey = $params['public_key']; |
|||
$this->privateKey = $params['private_key']; |
|||
} |
|||
|
|||
/** |
|||
* Returns the connection. |
|||
* |
|||
* @return \phpseclib\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 \phpseclib\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 RSA instance or null in case of a failure to load the key. |
|||
*/ |
|||
private function getPrivateKey() { |
|||
$key = new 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; |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue