diff options
Diffstat (limited to 'apps/files_external/3rdparty/icewind/smb')
11 files changed, 249 insertions, 54 deletions
diff --git a/apps/files_external/3rdparty/icewind/smb/README.md b/apps/files_external/3rdparty/icewind/smb/README.md index 272c4ebedcd..fec1faefbad 100644 --- a/apps/files_external/3rdparty/icewind/smb/README.md +++ b/apps/files_external/3rdparty/icewind/smb/README.md @@ -44,13 +44,42 @@ $server = $serverFactory->createServer('localhost', $auth); ### Using kerberos authentication ### +There are two ways of using kerberos to authenticate against the smb server: + +- Using a ticket from the php server +- Re-using a ticket send by the client + +### Using a server ticket + +Using a server ticket allows the web server to authenticate against the smb server using an existing machine account. + +The ticket needs to be available in the environment of the php process. + ```php $serverFactory = new ServerFactory(); $auth = new KerberosAuth(); $server = $serverFactory->createServer('localhost', $auth); ``` -Note that this requires a valid kerberos ticket to already be available for php +### Re-using a client ticket + +By re-using a client ticket you can create a single sign-on setup where the user authenticates against +the web service using kerberos. And the web server can forward that ticket to the smb server, allowing it +to act on the behalf of the user without requiring the user to enter his passord. + +The setup for such a system is fairly involved and requires roughly the following this + +- The web server is authenticated against kerberos with a machine account +- Delegation is enabled for the web server's machine account +- Apache is setup to perform kerberos authentication and save the ticket in it's environment +- Php has the krb5 extension installed +- The client authenticates using a ticket with forwarding enabled + +```php +$serverFactory = new ServerFactory(); +$auth = new KerberosApacheAuth(); +$server = $serverFactory->createServer('localhost', $auth); +``` ### Upload a file ### diff --git a/apps/files_external/3rdparty/icewind/smb/src/IShare.php b/apps/files_external/3rdparty/icewind/smb/src/IShare.php index 6ac6e0d2d15..40213b93a99 100644 --- a/apps/files_external/3rdparty/icewind/smb/src/IShare.php +++ b/apps/files_external/3rdparty/icewind/smb/src/IShare.php @@ -45,7 +45,7 @@ interface IShare { public function put(string $source, string $target): bool; /** - * Open a readable stream top a remote file + * Open a readable stream to a remote file * * @param string $source * @return resource a read only stream with the contents of the remote file diff --git a/apps/files_external/3rdparty/icewind/smb/src/KerberosApacheAuth.php b/apps/files_external/3rdparty/icewind/smb/src/KerberosApacheAuth.php new file mode 100644 index 00000000000..c49918be114 --- /dev/null +++ b/apps/files_external/3rdparty/icewind/smb/src/KerberosApacheAuth.php @@ -0,0 +1,136 @@ +<?php +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Icewind\SMB; + +use Icewind\SMB\Exception\DependencyException; +use Icewind\SMB\Exception\Exception; + +/** + * Use existing kerberos ticket to authenticate and reuse the apache ticket cache (mod_auth_kerb) + */ +class KerberosApacheAuth extends KerberosAuth implements IAuth { + /** @var string */ + private $ticketPath = ""; + + /** @var bool */ + private $init = false; + + /** @var string|false */ + private $ticketName; + + public function __construct() { + $this->ticketName = getenv("KRB5CCNAME"); + } + + + /** + * Copy the ticket to a temporary location and use that ticket for authentication + * + * @return void + */ + public function copyTicket(): void { + if (!$this->checkTicket()) { + return; + } + $krb5 = new \KRB5CCache(); + $krb5->open($this->ticketName); + $tmpFilename = tempnam("/tmp", "krb5cc_php_"); + $tmpCacheFile = "FILE:" . $tmpFilename; + $krb5->save($tmpCacheFile); + $this->ticketPath = $tmpFilename; + $this->ticketName = $tmpCacheFile; + } + + /** + * Pass the ticket to smbclient by memory instead of path + * + * @return void + */ + public function passTicketFromMemory(): void { + if (!$this->checkTicket()) { + return; + } + $krb5 = new \KRB5CCache(); + $krb5->open($this->ticketName); + $this->ticketName = (string)$krb5->getName(); + } + + /** + * Check if a valid kerberos ticket is present + * + * @return bool + * @psalm-assert-if-true string $this->ticketName + */ + public function checkTicket(): bool { + //read apache kerberos ticket cache + if (!$this->ticketName) { + return false; + } + + $krb5 = new \KRB5CCache(); + $krb5->open($this->ticketName); + /** @psalm-suppress MixedArgument */ + return count($krb5->getEntries()) > 0; + } + + private function init(): void { + if ($this->init) { + return; + } + $this->init = true; + // inspired by https://git.typo3.org/TYPO3CMS/Extensions/fal_cifs.git + + if (!extension_loaded("krb5")) { + // https://pecl.php.net/package/krb5 + throw new DependencyException('Ensure php-krb5 is installed.'); + } + + //read apache kerberos ticket cache + if (!$this->checkTicket()) { + throw new Exception('No kerberos ticket cache environment variable (KRB5CCNAME) found.'); + } + + // note that even if the ticketname is the value we got from `getenv("KRB5CCNAME")` we still need to set the env variable ourselves + // this is because `getenv` also reads the variables passed from the SAPI (apache-php) and we need to set the variable in the OS's env + putenv("KRB5CCNAME=" . $this->ticketName); + } + + public function getExtraCommandLineArguments(): string { + $this->init(); + return parent::getExtraCommandLineArguments(); + } + + public function setExtraSmbClientOptions($smbClientState): void { + $this->init(); + try { + parent::setExtraSmbClientOptions($smbClientState); + } catch (Exception $e) { + // suppress + } + } + + public function __destruct() { + if (!empty($this->ticketPath) && file_exists($this->ticketPath) && is_file($this->ticketPath)) { + unlink($this->ticketPath); + } + } +} diff --git a/apps/files_external/3rdparty/icewind/smb/src/Native/NativeFileInfo.php b/apps/files_external/3rdparty/icewind/smb/src/Native/NativeFileInfo.php index 539bb728426..85fb0274ac1 100644 --- a/apps/files_external/3rdparty/icewind/smb/src/Native/NativeFileInfo.php +++ b/apps/files_external/3rdparty/icewind/smb/src/Native/NativeFileInfo.php @@ -99,7 +99,7 @@ class NativeFileInfo implements IFileInfo { public function isDirectory(): bool { $mode = $this->getMode(); if ($mode > 0x1000) { - return (bool)($mode & 0x4000); // 0x4000: unix directory flag + return ($mode & 0x4000 && !($mode & 0x8000)); // 0x4000: unix directory flag shares bits with 0xC000: socket } else { return (bool)($mode & IFileInfo::MODE_DIRECTORY); } diff --git a/apps/files_external/3rdparty/icewind/smb/src/Native/NativeShare.php b/apps/files_external/3rdparty/icewind/smb/src/Native/NativeShare.php index 03ec501b830..8c4eab2a60f 100644 --- a/apps/files_external/3rdparty/icewind/smb/src/Native/NativeShare.php +++ b/apps/files_external/3rdparty/icewind/smb/src/Native/NativeShare.php @@ -267,14 +267,14 @@ class NativeShare extends AbstractShare { * Open a writeable stream to a remote file * Note: This method will truncate the file to 0bytes first * - * @param string $source + * @param string $target * @return resource a writeable stream * * @throws NotFoundException * @throws InvalidTypeException */ - public function write(string $source) { - $url = $this->buildUrl($source); + public function write(string $target) { + $url = $this->buildUrl($target); $handle = $this->getState()->create($url); return NativeWriteStream::wrap($this->getState(), $handle, 'w', $url); } @@ -282,14 +282,14 @@ class NativeShare extends AbstractShare { /** * Open a writeable stream and set the cursor to the end of the stream * - * @param string $source + * @param string $target * @return resource a writeable stream * * @throws NotFoundException * @throws InvalidTypeException */ - public function append(string $source) { - $url = $this->buildUrl($source); + public function append(string $target) { + $url = $this->buildUrl($target); $handle = $this->getState()->open($url, "a+"); return NativeWriteStream::wrap($this->getState(), $handle, "a", $url); } diff --git a/apps/files_external/3rdparty/icewind/smb/src/Native/NativeState.php b/apps/files_external/3rdparty/icewind/smb/src/Native/NativeState.php index e1a13ce3e72..088e6f733d1 100644 --- a/apps/files_external/3rdparty/icewind/smb/src/Native/NativeState.php +++ b/apps/files_external/3rdparty/icewind/smb/src/Native/NativeState.php @@ -38,6 +38,14 @@ class NativeState { /** @var bool */ protected $connected = false; + /** + * sync the garbage collection cycle + * __deconstruct() of KerberosAuth should not called too soon + * + * @var IAuth|null $auth + */ + protected $auth = null; + // see error.h const EXCEPTION_MAP = [ 1 => ForbiddenException::class, @@ -107,6 +115,11 @@ class NativeState { } $auth->setExtraSmbClientOptions($this->state); + + // sync the garbage collection cycle + // __deconstruct() of KerberosAuth should not caled too soon + $this->auth = $auth; + /** @var bool $result */ $result = @smbclient_state_init($this->state, $auth->getWorkgroup(), $auth->getUsername(), $auth->getPassword()); diff --git a/apps/files_external/3rdparty/icewind/smb/src/System.php b/apps/files_external/3rdparty/icewind/smb/src/System.php index 919907477ab..d3475e7a5cb 100644 --- a/apps/files_external/3rdparty/icewind/smb/src/System.php +++ b/apps/files_external/3rdparty/icewind/smb/src/System.php @@ -62,7 +62,7 @@ class System implements ISystem { $result = null; $output = []; exec("which $binary 2>&1", $output, $result); - $this->paths[$binary] = $result === 0 ? trim(implode('', $output)) : null; + $this->paths[$binary] = $result === 0 && isset($output[0]) ? (string)$output[0] : null; } return $this->paths[$binary]; } diff --git a/apps/files_external/3rdparty/icewind/smb/src/Wrapped/Connection.php b/apps/files_external/3rdparty/icewind/smb/src/Wrapped/Connection.php index 31b72b05d97..cc73ac1ad14 100644 --- a/apps/files_external/3rdparty/icewind/smb/src/Wrapped/Connection.php +++ b/apps/files_external/3rdparty/icewind/smb/src/Wrapped/Connection.php @@ -47,7 +47,7 @@ class Connection extends RawConnection { public function clearTillPrompt(): void { $this->write(''); do { - $promptLine = $this->readLine(); + $promptLine = $this->readTillPrompt(); if ($promptLine === false) { break; } @@ -56,13 +56,12 @@ class Connection extends RawConnection { if ($this->write('') === false) { throw new ConnectionRefusedException(); } - $this->readLine(); + $this->readTillPrompt(); } /** * get all unprocessed output from smbclient until the next prompt * - * @param (callable(string):bool)|null $callback (optional) callback to call for every line read * @return string[] * @throws AuthenticationException * @throws ConnectException @@ -71,42 +70,22 @@ class Connection extends RawConnection { * @throws NoLoginServerException * @throws AccessDeniedException */ - public function read(callable $callback = null): array { + public function read(): array { if (!$this->isValid()) { throw new ConnectionException('Connection not valid'); } - $promptLine = $this->readLine(); //first line is prompt - if ($promptLine === false) { - $this->unknownError($promptLine); - } - $this->parser->checkConnectionError($promptLine); - - $output = []; - if (!$this->isPrompt($promptLine)) { - $line = $promptLine; - } else { - $line = $this->readLine(); - } - if ($line === false) { - $this->unknownError($promptLine); - } - while ($line !== false && !$this->isPrompt($line)) { //next prompt functions as delimiter - if (is_callable($callback)) { - $result = $callback($line); - if ($result === false) { // allow the callback to close the connection for infinite running commands - $this->close(true); - break; - } - } else { - $output[] = $line; - } - $line = $this->readLine(); + $output = $this->readTillPrompt(); + if ($output === false) { + $this->unknownError(false); } + $output = explode("\n", $output); + // last line contains the prompt + array_pop($output); return $output; } private function isPrompt(string $line): bool { - return mb_substr($line, 0, self::DELIMITER_LENGTH) === self::DELIMITER; + return substr($line, 0, self::DELIMITER_LENGTH) === self::DELIMITER; } /** @@ -132,6 +111,6 @@ class Connection extends RawConnection { // ignore any errors while trying to send the close command, the process might already be dead @$this->write('close' . PHP_EOL); } - parent::close($terminate); + $this->close_process($terminate); } } diff --git a/apps/files_external/3rdparty/icewind/smb/src/Wrapped/NotifyHandler.php b/apps/files_external/3rdparty/icewind/smb/src/Wrapped/NotifyHandler.php index 18451f4daa6..ecb5bb1e3c1 100644 --- a/apps/files_external/3rdparty/icewind/smb/src/Wrapped/NotifyHandler.php +++ b/apps/files_external/3rdparty/icewind/smb/src/Wrapped/NotifyHandler.php @@ -65,16 +65,20 @@ class NotifyHandler implements INotifyHandler { */ public function listen(callable $callback): void { if ($this->listening) { - $this->connection->read(function (string $line) use ($callback): bool { + while (true) { + $line = $this->connection->readLine(); + if ($line === false) { + break; + } $this->checkForError($line); $change = $this->parseChangeLine($line); if ($change) { $result = $callback($change); - return $result === false ? false : true; - } else { - return true; + if ($result === false) { + break; + } } - }); + }; } } diff --git a/apps/files_external/3rdparty/icewind/smb/src/Wrapped/RawConnection.php b/apps/files_external/3rdparty/icewind/smb/src/Wrapped/RawConnection.php index 26a17cc584b..4aec674c3da 100644 --- a/apps/files_external/3rdparty/icewind/smb/src/Wrapped/RawConnection.php +++ b/apps/files_external/3rdparty/icewind/smb/src/Wrapped/RawConnection.php @@ -71,7 +71,8 @@ class RawConnection { setlocale(LC_ALL, Server::LOCALE); $env = array_merge($this->env, [ - 'CLI_FORCE_INTERACTIVE' => 'y', // Needed or the prompt isn't displayed!! + 'CLI_FORCE_INTERACTIVE' => 'y', // Make sure the prompt is displayed + 'CLI_NO_READLINE' => 1, // Not all distros build smbclient with readline, disable it to get consistent behaviour 'LC_ALL' => Server::LOCALE, 'LANG' => Server::LOCALE, 'COLUMNS' => 8192 // prevent smbclient from line-wrapping it's output @@ -91,7 +92,7 @@ class RawConnection { public function isValid(): bool { if (is_resource($this->process)) { $status = proc_get_status($this->process); - return (bool)$status['running']; + return $status['running']; } else { return false; } @@ -110,12 +111,29 @@ class RawConnection { } /** + * read output till the next prompt + * + * @return string|false + */ + public function readTillPrompt() { + $output = ""; + do { + $chunk = $this->readLine('\> '); + if ($chunk === false) { + return false; + } + $output .= $chunk; + } while (strlen($chunk) == 4096 && strpos($chunk, "smb:") === false); + return $output; + } + + /** * read a line of output * * @return string|false */ - public function readLine() { - return stream_get_line($this->getOutputStream(), 4086, "\n"); + public function readLine(string $end = "\n") { + return stream_get_line($this->getOutputStream(), 4096, $end); } /** @@ -202,6 +220,14 @@ class RawConnection { * @psalm-assert null $this->process */ public function close(bool $terminate = true): void { + $this->close_process($terminate); + } + + /** + * @param bool $terminate + * @psalm-assert null $this->process + */ + protected function close_process(bool $terminate = true): void { if (!is_resource($this->process)) { return; } diff --git a/apps/files_external/3rdparty/icewind/smb/src/Wrapped/Share.php b/apps/files_external/3rdparty/icewind/smb/src/Wrapped/Share.php index 68446d380e0..eb68d3800b3 100644 --- a/apps/files_external/3rdparty/icewind/smb/src/Wrapped/Share.php +++ b/apps/files_external/3rdparty/icewind/smb/src/Wrapped/Share.php @@ -345,11 +345,17 @@ class Share extends AbstractShare { // since returned stream is closed by the caller we need to create a new instance // since we can't re-use the same file descriptor over multiple calls $connection = $this->getConnection(); + stream_set_blocking($connection->getOutputStream(), false); $connection->write('get ' . $source . ' ' . $this->system->getFD(5)); $connection->write('exit'); $fh = $connection->getFileOutputStream(); - stream_context_set_option($fh, 'file', 'connection', $connection); + $fh = CallbackWrapper::wrap($fh, function() use ($connection) { + $connection->write(''); + }); + if (!is_resource($fh)) { + throw new Exception("Failed to wrap file output"); + } return $fh; } @@ -374,7 +380,9 @@ class Share extends AbstractShare { // use a close callback to ensure the upload is finished before continuing // this also serves as a way to keep the connection in scope - $stream = CallbackWrapper::wrap($fh, null, null, function () use ($connection) { + $stream = CallbackWrapper::wrap($fh, function() use ($connection) { + $connection->write(''); + }, null, function () use ($connection) { $connection->close(false); // dont terminate, give the upload some time }); if (is_resource($stream)) { @@ -446,7 +454,7 @@ class Share extends AbstractShare { * @return string[] */ protected function execute(string $command): array { - $this->connect()->write($command . PHP_EOL); + $this->connect()->write($command); return $this->connect()->read(); } |