migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,7 @@
.idea
vendor
composer.lock
.php_cs.cache
listen.php
test.php
*.cache
@@ -0,0 +1,19 @@
Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,187 @@
SMB
===
[![CI](https://github.com/icewind1991/SMB/actions/workflows/ci.yaml/badge.svg)](https://github.com/icewind1991/SMB/actions/workflows/ci.yaml)
[![codecov](https://codecov.io/gh/icewind1991/SMB/branch/master/graph/badge.svg?token=eTg0P466k6)](https://codecov.io/gh/icewind1991/SMB)
PHP wrapper for `smbclient` and [`libsmbclient-php`](https://github.com/eduardok/libsmbclient-php)
- Reuses a single `smbclient` instance for multiple requests
- Doesn't leak the password to the process list
- Simple 1-on-1 mapping of SMB commands
- A stream-based api to remove the need for temporary files
- Support for using libsmbclient directly trough [`libsmbclient-php`](https://github.com/eduardok/libsmbclient-php)
Examples
----
### Connect to a share ###
```php
<?php
use Icewind\SMB\ServerFactory;
use Icewind\SMB\BasicAuth;
require('vendor/autoload.php');
$serverFactory = new ServerFactory();
$auth = new BasicAuth('user', 'workgroup', 'password');
$server = $serverFactory->createServer('localhost', $auth);
$share = $server->getShare('test');
```
The server factory will automatically pick between the `smbclient` and `libsmbclient-php`
based backend depending on what is available.
### Using anonymous authentication ###
```php
$serverFactory = new ServerFactory();
$auth = new AnonymousAuth();
$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);
```
### 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 ###
```php
$share->put($fileToUpload, 'example.txt');
```
### Download a file ###
```php
$share->get('example.txt', $target);
```
### List shares on the remote server ###
```php
$shares = $server->listShares();
foreach ($shares as $share) {
echo $share->getName() . "\n";
}
```
### List the content of a folder ###
```php
$content = $share->dir('test');
foreach ($content as $info) {
echo $info->getName() . "\n";
echo "\tsize :" . $info->getSize() . "\n";
}
```
### Using read streams
```php
$fh = $share->read('test.txt');
echo fread($fh, 4086);
fclose($fh);
```
### Using write streams
```php
$fh = $share->write('test.txt');
fwrite($fh, 'bar');
fclose($fh);
```
**Note**: write() will truncate your file to 0bytes. You may open a writeable stream with append() which will point
the cursor to the end of the file or create it if it does not exist yet. (append() is only compatible with libsmbclient-php)
```php
$fh = $share->append('test.txt');
fwrite($fh, 'bar');
fclose($fh);
```
### Using notify
```php
$share->notify('')->listen(function (\Icewind\SMB\Change $change) {
echo $change->getCode() . ': ' . $change->getPath() . "\n";
});
```
### Changing network timeouts
```php
$options = new Options();
$options->setTimeout(5);
$serverFactory = new ServerFactory($options);
```
### Setting protocol version
```php
$options = new Options();
$options->setMinProtocol(IOptions::PROTOCOL_SMB2);
$options->setMaxProtocol(IOptions::PROTOCOL_SMB3);
$serverFactory = new ServerFactory($options);
```
Note, setting the protocol version is not supported with php-smbclient version 1.0.1 or lower.
### Customizing system integration
The `smbclient` backend needs to get various information about the system it's running on to function
such as the paths of various binaries or the system timezone.
While the default logic for getting this information should work on most systems, it is possible to customize this behaviour.
In order to customize the integration you provide a custom implementation of `ITimezoneProvider` and/or `ISystem` and pass them as arguments to the `ServerFactory`.
## Testing SMB
Use the following steps to check if the library can connect to your SMB share.
1. Clone this repository or download the source as [zip](https://github.com/icewind1991/SMB/archive/master.zip)
2. Make sure [composer](https://getcomposer.org/) is installed
3. Run `composer install` in the root of the repository
4. Edit `example.php` with the relevant settings for your share.
5. Run `php example.php`
If everything works correctly then the contents of the share should be outputted.
@@ -0,0 +1,37 @@
{
"name": "icewind/smb",
"description": "php wrapper for smbclient and libsmbclient-php",
"license": "MIT",
"authors": [
{
"name": "Robin Appelman",
"email": "icewind@owncloud.com"
}
],
"require": {
"php": ">=7.2",
"icewind/streams": ">=0.7.3"
},
"require-dev": {
"phpunit/phpunit": "^8.5|^9.3.8",
"friendsofphp/php-cs-fixer": "^2.16",
"phpstan/phpstan": "^0.12.57",
"psalm/phar": "^4.3"
},
"autoload": {
"psr-4": {
"Icewind\\SMB\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Icewind\\SMB\\Test\\": "tests/"
}
},
"scripts": {
"lint": "parallel-lint --exclude src --exclude vendor --exclude target --exclude build .",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
"psalm": "psalm.phar"
}
}
@@ -0,0 +1,84 @@
<?php declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 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;
class ACL {
const TYPE_ALLOW = 0;
const TYPE_DENY = 1;
const MASK_READ = 0x0001;
const MASK_WRITE = 0x0002;
const MASK_EXECUTE = 0x00020;
const MASK_DELETE = 0x10000;
const FLAG_OBJECT_INHERIT = 0x1;
const FLAG_CONTAINER_INHERIT = 0x2;
/** @var int */
private $type;
/** @var int */
private $flags;
/** @var int */
private $mask;
public function __construct(int $type, int $flags, int $mask) {
$this->type = $type;
$this->flags = $flags;
$this->mask = $mask;
}
/**
* Check if the acl allows a specific permissions
*
* Note that this does not take inherited acls into account
*
* @param int $mask one of the ACL::MASK_* constants
* @return bool
*/
public function allows(int $mask): bool {
return $this->type === self::TYPE_ALLOW && ($this->mask & $mask) === $mask;
}
/**
* Check if the acl allows a specific permissions
*
* Note that this does not take inherited acls into account
*
* @param int $mask one of the ACL::MASK_* constants
* @return bool
*/
public function denies(int $mask): bool {
return $this->type === self::TYPE_DENY && ($this->mask & $mask) === $mask;
}
public function getType(): int {
return $this->type;
}
public function getFlags(): int {
return $this->flags;
}
public function getMask(): int {
return $this->mask;
}
}
@@ -0,0 +1,76 @@
<?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;
abstract class AbstractServer implements IServer {
const LOCALE = 'en_US.UTF-8';
/** @var string */
protected $host;
/** @var IAuth */
protected $auth;
/** @var ISystem */
protected $system;
/** @var ITimeZoneProvider */
protected $timezoneProvider;
/** @var IOptions */
protected $options;
/**
* @param string $host
* @param IAuth $auth
* @param ISystem $system
* @param ITimeZoneProvider $timeZoneProvider
* @param IOptions $options
*/
public function __construct(string $host, IAuth $auth, ISystem $system, ITimeZoneProvider $timeZoneProvider, IOptions $options) {
$this->host = $host;
$this->auth = $auth;
$this->system = $system;
$this->timezoneProvider = $timeZoneProvider;
$this->options = $options;
}
public function getAuth(): IAuth {
return $this->auth;
}
public function getHost(): string {
return $this->host;
}
public function getTimeZone(): string {
return $this->timezoneProvider->get($this->host);
}
public function getSystem(): ISystem {
return $this->system;
}
public function getOptions(): IOptions {
return $this->options;
}
}
@@ -0,0 +1,38 @@
<?php
/**
* Copyright (c) 2015 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB;
use Icewind\SMB\Exception\InvalidPathException;
abstract class AbstractShare implements IShare {
/** @var string[] */
private $forbiddenCharacters;
public function __construct() {
$this->forbiddenCharacters = ['?', '<', '>', ':', '*', '|', '"', chr(0), "\n", "\r"];
}
/**
* @param string $path
* @throws InvalidPathException
*/
protected function verifyPath(string $path): void {
foreach ($this->forbiddenCharacters as $char) {
if (strpos($path, $char) !== false) {
throw new InvalidPathException('Invalid path, "' . $char . '" is not allowed');
}
}
}
/**
* @param string[] $charList
*/
public function setForbiddenChars(array $charList): void {
$this->forbiddenCharacters = $charList;
}
}
@@ -0,0 +1,48 @@
<?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\Exception;
class AnonymousAuth implements IAuth {
public function getUsername(): ?string {
return null;
}
public function getWorkgroup(): ?string {
return 'dummy';
}
public function getPassword(): ?string {
return null;
}
public function getExtraCommandLineArguments(): string {
return '-N';
}
public function setExtraSmbClientOptions($smbClientState): void {
if (smbclient_option_set($smbClientState, SMBCLIENT_OPT_AUTO_ANONYMOUS_LOGIN, true) === false) {
throw new Exception("Failed to set smbclient options for anonymous auth");
}
}
}
@@ -0,0 +1,57 @@
<?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;
class BasicAuth implements IAuth {
/** @var string */
private $username;
/** @var string|null */
private $workgroup;
/** @var string */
private $password;
public function __construct(string $username, ?string $workgroup, string $password) {
$this->username = $username;
$this->workgroup = $workgroup;
$this->password = $password;
}
public function getUsername(): ?string {
return $this->username;
}
public function getWorkgroup(): ?string {
return $this->workgroup;
}
public function getPassword(): ?string {
return $this->password;
}
public function getExtraCommandLineArguments(): string {
return ($this->workgroup) ? '-W ' . escapeshellarg($this->workgroup) : '';
}
public function setExtraSmbClientOptions($smbClientState): void {
// noop
}
}
@@ -0,0 +1,29 @@
<?php
/**
* @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*
*/
namespace Icewind\SMB;
class Change {
/** @var int */
private $code;
/** @var string */
private $path;
public function __construct(int $code, string $path) {
$this->code = $code;
$this->path = $path;
}
public function getCode(): int {
return $this->code;
}
public function getPath(): string {
return $this->path;
}
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class AccessDeniedException extends ConnectException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class AlreadyExistsException extends InvalidRequestException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class AuthenticationException extends ConnectException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class ConnectException extends Exception {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2020 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class ConnectionAbortedException extends ConnectException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class ConnectionException extends ConnectException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class ConnectionRefusedException extends ConnectException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2020 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class ConnectionResetException extends ConnectException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2016 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class DependencyException extends Exception {
}
@@ -0,0 +1,52 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
use Throwable;
/**
* @psalm-consistent-constructor
*/
class Exception extends \Exception {
public function __construct(string $message = "", int $code = 0, Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
/**
* @param string|null $path
* @param string|int|null $error
* @return Exception
*/
public static function unknown(?string $path, $error): Exception {
$message = 'Unknown error (' . (string)$error . ')';
if ($path) {
$message .= ' for ' . $path;
}
return new Exception($message, is_int($error) ? $error : 0);
}
/**
* @param array<int|string, class-string<Exception>> $exceptionMap
* @param string|int|null $error
* @param string|null $path
* @return Exception
*/
public static function fromMap(array $exceptionMap, $error, ?string $path): Exception {
if (isset($exceptionMap[$error])) {
$exceptionClass = $exceptionMap[$error];
if (is_numeric($error)) {
return new $exceptionClass($path, $error);
} else {
return new $exceptionClass($path);
}
} else {
return Exception::unknown($path, $error);
}
}
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class FileInUseException extends InvalidRequestException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class ForbiddenException extends InvalidRequestException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class HostDownException extends ConnectException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class InvalidArgumentException extends InvalidRequestException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class InvalidHostException extends ConnectException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class InvalidParameterException extends InvalidRequestException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class InvalidPathException extends InvalidRequestException {
}
@@ -0,0 +1,30 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class InvalidRequestException extends Exception {
/**
* @var string
*/
protected $path;
public function __construct(string $path = "", int $code = 0, \Throwable $previous = null) {
$class = get_class($this);
$parts = explode('\\', $class);
$baseName = array_pop($parts);
parent::__construct('Invalid request for ' . $path . ' (' . $baseName . ')', $code, $previous);
$this->path = $path;
}
/**
* @return string
*/
public function getPath() {
return $this->path;
}
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class InvalidResourceException extends Exception {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class InvalidTypeException extends InvalidRequestException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class NoLoginServerException extends ConnectException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class NoRouteToHostException extends ConnectException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class NotEmptyException extends InvalidRequestException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class NotFoundException extends InvalidRequestException {
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class OutOfSpaceException extends InvalidRequestException {
}
@@ -0,0 +1,16 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
use Throwable;
class RevisionMismatchException extends Exception {
public function __construct(string $message = 'Protocol version mismatch', int $code = 0, Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}
@@ -0,0 +1,11 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Exception;
class TimedOutException extends ConnectException {
}
@@ -0,0 +1,44 @@
<?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;
interface IAuth {
public function getUsername(): ?string;
public function getWorkgroup(): ?string;
public function getPassword(): ?string;
/**
* Any extra command line option for smbclient that are required
*
* @return string
*/
public function getExtraCommandLineArguments(): string;
/**
* Set any extra options for libsmbclient that are required
*
* @param resource $smbClientState
*/
public function setExtraSmbClientOptions($smbClientState): void;
}
@@ -0,0 +1,46 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB;
interface IFileInfo {
/*
* Mappings of the DOS mode bits, as returned by smbc_getxattr() when the
* attribute name "system.dos_attr.mode" (or "system.dos_attr.*" or
* "system.*") is specified.
*/
const MODE_READONLY = 0x01;
const MODE_HIDDEN = 0x02;
const MODE_SYSTEM = 0x04;
const MODE_VOLUME_ID = 0x08;
const MODE_DIRECTORY = 0x10;
const MODE_ARCHIVE = 0x20;
const MODE_NORMAL = 0x80;
public function getPath(): string;
public function getName(): string;
public function getSize(): int;
public function getMTime(): int;
public function isDirectory(): bool;
public function isReadOnly(): bool;
public function isHidden(): bool;
public function isSystem(): bool;
public function isArchived(): bool;
/**
* @return ACL[]
*/
public function getAcls(): array;
}
@@ -0,0 +1,45 @@
<?php
/**
* @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*
*/
namespace Icewind\SMB;
interface INotifyHandler {
// https://msdn.microsoft.com/en-us/library/dn392331.aspx
const NOTIFY_ADDED = 1;
const NOTIFY_REMOVED = 2;
const NOTIFY_MODIFIED = 3;
const NOTIFY_RENAMED_OLD = 4;
const NOTIFY_RENAMED_NEW = 5;
const NOTIFY_ADDED_STREAM = 6;
const NOTIFY_REMOVED_STREAM = 7;
const NOTIFY_MODIFIED_STREAM = 8;
const NOTIFY_REMOVED_BY_DELETE = 9;
/**
* Get all changes detected since the start of the notify process or the last call to getChanges
*
* @return Change[]
*/
public function getChanges(): array;
/**
* Listen actively to all incoming changes
*
* Note that this is a blocking process and will cause the process to block forever if not explicitly terminated
*
* @param callable(Change):?bool $callback
*/
public function listen(callable $callback): void;
/**
* Stop listening for changes
*
* Note that any pending changes will be discarded
*/
public function stop(): void;
}
@@ -0,0 +1,41 @@
<?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;
interface IOptions {
const PROTOCOL_NT1 = 'NT1';
const PROTOCOL_SMB2 = 'SMB2';
const PROTOCOL_SMB2_02 = 'SMB2_02';
const PROTOCOL_SMB2_22 = 'SMB2_22';
const PROTOCOL_SMB2_24 = 'SMB2_24';
const PROTOCOL_SMB3 = 'SMB3';
const PROTOCOL_SMB3_00 = 'SMB3_00';
const PROTOCOL_SMB3_02 = 'SMB3_02';
const PROTOCOL_SMB3_10 = 'SMB3_10';
const PROTOCOL_SMB3_11 = 'SMB3_11';
public function getTimeout(): int;
public function getMinProtocol(): ?string;
public function getMaxProtocol(): ?string;
}
@@ -0,0 +1,46 @@
<?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;
interface IServer {
public function getAuth(): IAuth;
public function getHost(): string;
/**
* @return \Icewind\SMB\IShare[]
*
* @throws \Icewind\SMB\Exception\AuthenticationException
* @throws \Icewind\SMB\Exception\InvalidHostException
*/
public function listShares(): array;
public function getShare(string $name): IShare;
public function getTimeZone(): string;
public function getSystem(): ISystem;
public function getOptions(): IOptions;
public static function available(ISystem $system): bool;
}
@@ -0,0 +1,165 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB;
use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\InvalidRequestException;
use Icewind\SMB\Exception\InvalidTypeException;
use Icewind\SMB\Exception\NotFoundException;
interface IShare {
/**
* Get the name of the share
*
* @return string
*/
public function getName(): string;
/**
* Download a remote file
*
* @param string $source remove file
* @param string $target local file
* @return bool
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function get(string $source, string $target): bool;
/**
* Upload a local file
*
* @param string $source local file
* @param string $target remove file
* @return bool
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function put(string $source, string $target): bool;
/**
* Open a readable stream to a remote file
*
* @param string $source
* @return resource a read only stream with the contents of the remote file
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function read(string $source);
/**
* Open a writable stream to a remote file
* Note: This method will truncate the file to 0bytes
*
* @param string $target
* @return resource a write only stream to upload a remote file
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function write(string $target);
/**
* Open a writable stream to a remote file and set the cursor to the end of the file
*
* @param string $target
* @return resource a write only stream to upload a remote file
*
* @throws NotFoundException
* @throws InvalidTypeException
* @throws InvalidRequestException
*/
public function append(string $target);
/**
* Rename a remote file
*
* @param string $from
* @param string $to
* @return bool
*
* @throws NotFoundException
* @throws AlreadyExistsException
*/
public function rename(string $from, string $to): bool;
/**
* Delete a file on the share
*
* @param string $path
* @return bool
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function del(string $path): bool;
/**
* List the content of a remote folder
*
* @param string $path
* @return IFileInfo[]
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function dir(string $path): array;
/**
* @param string $path
* @return IFileInfo
*
* @throws NotFoundException
*/
public function stat(string $path): IFileInfo;
/**
* Create a folder on the share
*
* @param string $path
* @return bool
*
* @throws NotFoundException
* @throws AlreadyExistsException
*/
public function mkdir(string $path): bool;
/**
* Remove a folder on the share
*
* @param string $path
* @return bool
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function rmdir(string $path): bool;
/**
* @param string $path
* @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
* @return mixed
*/
public function setMode(string $path, int $mode);
/**
* @param string $path
* @return INotifyHandler
*/
public function notify(string $path);
/**
* Get the IServer instance for this share
*
* @return IServer
*/
public function getServer(): IServer;
}
@@ -0,0 +1,78 @@
<?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;
/**
* The `ISystem` interface provides a way to access system dependent information
* such as the availability and location of certain binaries.
*/
interface ISystem {
/**
* Get the path to a file descriptor of the current process
*
* @param int $num the file descriptor id
* @return string
*/
public function getFD(int $num): string;
/**
* Get the full path to the `smbclient` binary of null if the binary is not available
*
* @return string|null
*/
public function getSmbclientPath(): ?string;
/**
* Get the full path to the `net` binary of null if the binary is not available
*
* @return string|null
*/
public function getNetPath(): ?string;
/**
* Get the full path to the `smbcacls` binary of null if the binary is not available
*
* @return string|null
*/
public function getSmbcAclsPath(): ?string;
/**
* Get the full path to the `stdbuf` binary of null if the binary is not available
*
* @return string|null
*/
public function getStdBufPath(): ?string;
/**
* Get the full path to the `date` binary of null if the binary is not available
*
* @return string|null
*/
public function getDatePath(): ?string;
/**
* Whether or not the smbclient php extension is enabled
*
* @return bool
*/
public function libSmbclientAvailable(): bool;
}
@@ -0,0 +1,32 @@
<?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;
interface ITimeZoneProvider {
/**
* Get the timezone of the smb server
*
* @param string $host
* @return string
*/
public function get(string $host): string;
}
@@ -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);
}
}
}
@@ -0,0 +1,54 @@
<?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\Exception;
/**
* Use existing kerberos ticket to authenticate
*/
class KerberosAuth implements IAuth {
public function getUsername(): ?string {
return 'dummy';
}
public function getWorkgroup(): ?string {
return 'dummy';
}
public function getPassword(): ?string {
return null;
}
public function getExtraCommandLineArguments(): string {
return '-k';
}
public function setExtraSmbClientOptions($smbClientState): void {
$success = (bool)smbclient_option_set($smbClientState, SMBCLIENT_OPT_USE_KERBEROS, true);
$success = $success && smbclient_option_set($smbClientState, SMBCLIENT_OPT_FALLBACK_AFTER_KERBEROS, false);
if (!$success) {
throw new Exception("Failed to set smbclient options for kerberos auth");
}
}
}
@@ -0,0 +1,162 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Native;
use Icewind\SMB\ACL;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\IFileInfo;
class NativeFileInfo implements IFileInfo {
/** @var string */
protected $path;
/** @var string */
protected $name;
/** @var NativeShare */
protected $share;
/** @var array{"mode": int, "size": int, "write_time": int}|null */
protected $attributeCache = null;
public function __construct(NativeShare $share, string $path, string $name) {
$this->share = $share;
$this->path = $path;
$this->name = $name;
}
public function getPath(): string {
return $this->path;
}
public function getName(): string {
return $this->name;
}
/**
* @return array{"mode": int, "size": int, "write_time": int}
*/
protected function stat(): array {
if (is_null($this->attributeCache)) {
$rawAttributes = explode(',', $this->share->getAttribute($this->path, 'system.dos_attr.*'));
$attributes = [];
foreach ($rawAttributes as $rawAttribute) {
list($name, $value) = explode(':', $rawAttribute);
$name = strtolower($name);
if ($name == 'mode') {
$attributes[$name] = (int)hexdec(substr($value, 2));
} else {
$attributes[$name] = (int)$value;
}
}
if (!isset($attributes['mode'])) {
throw new Exception("Invalid attribute response");
}
if (!isset($attributes['size'])) {
throw new Exception("Invalid attribute response");
}
if (!isset($attributes['write_time'])) {
throw new Exception("Invalid attribute response");
}
$this->attributeCache = $attributes;
}
return $this->attributeCache;
}
public function getSize(): int {
$stat = $this->stat();
return $stat['size'];
}
public function getMTime(): int {
$stat = $this->stat();
return $stat['write_time'];
}
/**
* On "mode":
*
* different smbclient versions seem to return different mode values for 'system.dos_attr.mode'
*
* older versions return the dos permissions mask as defined in `IFileInfo::MODE_*` while
* newer versions return the equivalent unix permission mask.
*
* Since the unix mask doesn't contain the proper hidden/archive/system flags we have to assume them
* as false (except for `hidden` where we use the unix dotfile convention)
*/
protected function getMode(): int {
$mode = $this->stat()['mode'];
// Let us ignore the ATTR_NOT_CONTENT_INDEXED for now
$mode &= ~0x00002000;
return $mode;
}
public function isDirectory(): bool {
$mode = $this->getMode();
if ($mode > 0x1000) {
return ($mode & 0x4000 && !($mode & 0x8000)); // 0x4000: unix directory flag shares bits with 0xC000: socket
} else {
return (bool)($mode & IFileInfo::MODE_DIRECTORY);
}
}
public function isReadOnly(): bool {
$mode = $this->getMode();
if ($mode > 0x1000) {
return !(bool)($mode & 0x80); // 0x80: owner write permissions
} else {
return (bool)($mode & IFileInfo::MODE_READONLY);
}
}
public function isHidden(): bool {
$mode = $this->getMode();
if ($mode > 0x1000) {
return strlen($this->name) > 0 && $this->name[0] === '.';
} else {
return (bool)($mode & IFileInfo::MODE_HIDDEN);
}
}
public function isSystem(): bool {
$mode = $this->getMode();
if ($mode > 0x1000) {
return false;
} else {
return (bool)($mode & IFileInfo::MODE_SYSTEM);
}
}
public function isArchived(): bool {
$mode = $this->getMode();
if ($mode > 0x1000) {
return false;
} else {
return (bool)($mode & IFileInfo::MODE_ARCHIVE);
}
}
/**
* @return ACL[]
*/
public function getAcls(): array {
$acls = [];
$attribute = $this->share->getAttribute($this->path, 'system.nt_sec_desc.acl.*+');
foreach (explode(',', $attribute) as $acl) {
list($user, $permissions) = explode(':', $acl, 2);
$user = trim($user, '\\');
list($type, $flags, $mask) = explode('/', $permissions);
$mask = hexdec($mask);
$acls[$user] = new ACL((int)$type, (int)$flags, (int)$mask);
}
return $acls;
}
}
@@ -0,0 +1,93 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Native;
use Icewind\SMB\StringBuffer;
/**
* Stream optimized for read only usage
*/
class NativeReadStream extends NativeStream {
const CHUNK_SIZE = 1048576; // 1MB chunks
/** @var StringBuffer */
private $readBuffer;
public function __construct() {
$this->readBuffer = new StringBuffer();
}
/** @var int */
private $pos = 0;
public function stream_open($path, $mode, $options, &$opened_path) {
return parent::stream_open($path, $mode, $options, $opened_path);
}
/**
* Wrap a stream from libsmbclient-php into a regular php stream
*
* @param NativeState $state
* @param resource $smbStream
* @param string $mode
* @param string $url
* @return resource
*/
public static function wrap(NativeState $state, $smbStream, string $mode, string $url) {
return parent::wrapClass($state, $smbStream, $mode, $url, NativeReadStream::class);
}
public function stream_read($count) {
// php reads 8192 bytes at once
// however due to network latency etc, it's faster to read in larger chunks
// and buffer the result
if (!parent::stream_eof() && $this->readBuffer->remaining() < $count) {
$chunk = parent::stream_read(self::CHUNK_SIZE);
if ($chunk === false) {
return false;
}
$this->readBuffer->push($chunk);
}
$result = $this->readBuffer->read($count);
$read = strlen($result);
$this->pos += $read;
return $result;
}
public function stream_seek($offset, $whence = SEEK_SET) {
$result = parent::stream_seek($offset, $whence);
if ($result) {
$this->readBuffer->clear();
$pos = parent::stream_tell();
if ($pos === false) {
return false;
}
$this->pos = $pos;
}
return $result;
}
public function stream_eof() {
return $this->readBuffer->remaining() <= 0 && parent::stream_eof();
}
public function stream_tell() {
return $this->pos;
}
public function stream_write($data) {
return false;
}
public function stream_truncate($size) {
return false;
}
}
@@ -0,0 +1,65 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Native;
use Icewind\SMB\AbstractServer;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\IAuth;
use Icewind\SMB\IOptions;
use Icewind\SMB\IShare;
use Icewind\SMB\ISystem;
use Icewind\SMB\ITimeZoneProvider;
class NativeServer extends AbstractServer {
/**
* @var NativeState
*/
protected $state;
public function __construct(string $host, IAuth $auth, ISystem $system, ITimeZoneProvider $timeZoneProvider, IOptions $options) {
parent::__construct($host, $auth, $system, $timeZoneProvider, $options);
$this->state = new NativeState();
}
protected function connect(): void {
$this->state->init($this->getAuth(), $this->getOptions());
}
/**
* @return IShare[]
* @throws AuthenticationException
* @throws InvalidHostException
*/
public function listShares(): array {
$this->connect();
$shares = [];
$dh = $this->state->opendir('smb://' . $this->getHost());
while ($share = $this->state->readdir($dh, '')) {
if ($share['type'] === 'file share') {
$shares[] = $this->getShare($share['name']);
}
}
$this->state->closedir($dh, '');
return $shares;
}
public function getShare(string $name): IShare {
return new NativeShare($this, $name);
}
/**
* Check if the smbclient php extension is available
*
* @param ISystem $system
* @return bool
*/
public static function available(ISystem $system): bool {
return $system->libSmbclientAvailable();
}
}
@@ -0,0 +1,363 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Native;
use Icewind\SMB\AbstractShare;
use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\DependencyException;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\Exception\InvalidPathException;
use Icewind\SMB\Exception\InvalidResourceException;
use Icewind\SMB\Exception\InvalidTypeException;
use Icewind\SMB\Exception\NotFoundException;
use Icewind\SMB\IFileInfo;
use Icewind\SMB\INotifyHandler;
use Icewind\SMB\IServer;
use Icewind\SMB\Wrapped\Server;
use Icewind\SMB\Wrapped\Share;
class NativeShare extends AbstractShare {
/**
* @var IServer $server
*/
private $server;
/**
* @var string $name
*/
private $name;
/** @var NativeState|null $state */
private $state = null;
public function __construct(IServer $server, string $name) {
parent::__construct();
$this->server = $server;
$this->name = $name;
}
/**
* @throws ConnectionException
* @throws AuthenticationException
* @throws InvalidHostException
*/
protected function getState(): NativeState {
if ($this->state) {
return $this->state;
}
$this->state = new NativeState();
$this->state->init($this->server->getAuth(), $this->server->getOptions());
return $this->state;
}
/**
* Get the name of the share
*
* @return string
*/
public function getName(): string {
return $this->name;
}
private function buildUrl(string $path): string {
$this->verifyPath($path);
$url = sprintf('smb://%s/%s', $this->server->getHost(), $this->name);
if ($path) {
$path = trim($path, '/');
$url .= '/';
$url .= implode('/', array_map('rawurlencode', explode('/', $path)));
}
return $url;
}
/**
* List the content of a remote folder
*
* @param string $path
* @return IFileInfo[]
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function dir(string $path): array {
$files = [];
$dh = $this->getState()->opendir($this->buildUrl($path));
while ($file = $this->getState()->readdir($dh, $path)) {
$name = $file['name'];
if ($name !== '.' and $name !== '..') {
$fullPath = $path . '/' . $name;
$files [] = new NativeFileInfo($this, $fullPath, $name);
}
}
$this->getState()->closedir($dh, $path);
return $files;
}
/**
* @param string $path
* @return IFileInfo
*/
public function stat(string $path): IFileInfo {
$info = new NativeFileInfo($this, $path, self::mb_basename($path));
// trigger attribute loading
$info->getSize();
return $info;
}
/**
* Multibyte unicode safe version of basename()
*
* @param string $path
* @link http://php.net/manual/en/function.basename.php#121405
* @return string
*/
protected static function mb_basename(string $path): string {
if (preg_match('@^.*[\\\\/]([^\\\\/]+)$@s', $path, $matches)) {
return $matches[1];
} elseif (preg_match('@^([^\\\\/]+)$@s', $path, $matches)) {
return $matches[1];
}
return '';
}
/**
* Create a folder on the share
*
* @param string $path
* @return bool
*
* @throws NotFoundException
* @throws AlreadyExistsException
*/
public function mkdir(string $path): bool {
return $this->getState()->mkdir($this->buildUrl($path));
}
/**
* Remove a folder on the share
*
* @param string $path
* @return bool
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function rmdir(string $path): bool {
return $this->getState()->rmdir($this->buildUrl($path));
}
/**
* Delete a file on the share
*
* @param string $path
* @return bool
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function del(string $path): bool {
return $this->getState()->unlink($this->buildUrl($path));
}
/**
* Rename a remote file
*
* @param string $from
* @param string $to
* @return bool
*
* @throws NotFoundException
* @throws AlreadyExistsException
*/
public function rename(string $from, string $to): bool {
return $this->getState()->rename($this->buildUrl($from), $this->buildUrl($to));
}
/**
* Upload a local file
*
* @param string $source local file
* @param string $target remove file
* @return bool
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function put(string $source, string $target): bool {
$sourceHandle = fopen($source, 'rb');
$targetUrl = $this->buildUrl($target);
$targetHandle = $this->getState()->create($targetUrl);
while ($data = fread($sourceHandle, NativeReadStream::CHUNK_SIZE)) {
$this->getState()->write($targetHandle, $data, $targetUrl);
}
$this->getState()->close($targetHandle, $targetUrl);
return true;
}
/**
* Download a remote file
*
* @param string $source remove file
* @param string $target local file
* @return bool
*
* @throws AuthenticationException
* @throws ConnectionException
* @throws InvalidHostException
* @throws InvalidPathException
* @throws InvalidResourceException
*/
public function get(string $source, string $target): bool {
if (!$target) {
throw new InvalidPathException('Invalid target path: Filename cannot be empty');
}
$sourceHandle = $this->getState()->open($this->buildUrl($source), 'r');
$targetHandle = @fopen($target, 'wb');
if (!$targetHandle) {
$error = error_get_last();
if (is_array($error)) {
$reason = $error['message'];
} else {
$reason = 'Unknown error';
}
$this->getState()->close($sourceHandle, $this->buildUrl($source));
throw new InvalidResourceException('Failed opening local file "' . $target . '" for writing: ' . $reason);
}
while ($data = $this->getState()->read($sourceHandle, NativeReadStream::CHUNK_SIZE, $source)) {
fwrite($targetHandle, $data);
}
$this->getState()->close($sourceHandle, $this->buildUrl($source));
return true;
}
/**
* Open a readable stream to a remote file
*
* @param string $source
* @return resource a read only stream with the contents of the remote file
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function read(string $source) {
$url = $this->buildUrl($source);
$handle = $this->getState()->open($url, 'r');
return NativeReadStream::wrap($this->getState(), $handle, 'r', $url);
}
/**
* Open a writeable stream to a remote file
* Note: This method will truncate the file to 0bytes first
*
* @param string $target
* @return resource a writeable stream
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function write(string $target) {
$url = $this->buildUrl($target);
$handle = $this->getState()->create($url);
return NativeWriteStream::wrap($this->getState(), $handle, 'w', $url);
}
/**
* Open a writeable stream and set the cursor to the end of the stream
*
* @param string $target
* @return resource a writeable stream
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function append(string $target) {
$url = $this->buildUrl($target);
$handle = $this->getState()->open($url, "a+");
return NativeWriteStream::wrap($this->getState(), $handle, "a", $url);
}
/**
* Get extended attributes for the path
*
* @param string $path
* @param string $attribute attribute to get the info
* @return string the attribute value
*/
public function getAttribute(string $path, string $attribute): string {
return $this->getState()->getxattr($this->buildUrl($path), $attribute);
}
/**
* Set extended attributes for the given path
*
* @param string $path
* @param string $attribute attribute to get the info
* @param string|int $value
* @return mixed the attribute value
*/
public function setAttribute(string $path, string $attribute, $value) {
if (is_int($value)) {
if ($attribute === 'system.dos_attr.mode') {
$value = '0x' . dechex($value);
} else {
throw new \InvalidArgumentException("Invalid value for attribute");
}
}
return $this->getState()->setxattr($this->buildUrl($path), $attribute, $value);
}
/**
* Set DOS comaptible node mode
*
* @param string $path
* @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
* @return mixed
*/
public function setMode(string $path, int $mode) {
return $this->setAttribute($path, 'system.dos_attr.mode', $mode);
}
/**
* Start smb notify listener
* Note: This is a blocking call
*
* @param string $path
* @return INotifyHandler
*/
public function notify(string $path): INotifyHandler {
// php-smbclient does not support notify (https://github.com/eduardok/libsmbclient-php/issues/29)
// so we use the smbclient based backend for this
if (!Server::available($this->server->getSystem())) {
throw new DependencyException('smbclient not found in path for notify command');
}
$share = new Share($this->server, $this->getName(), $this->server->getSystem());
return $share->notify($path);
}
public function getServer(): IServer {
return $this->server;
}
public function __destruct() {
unset($this->state);
}
}
@@ -0,0 +1,377 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Native;
use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\ConnectionRefusedException;
use Icewind\SMB\Exception\ConnectionResetException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\FileInUseException;
use Icewind\SMB\Exception\ForbiddenException;
use Icewind\SMB\Exception\HostDownException;
use Icewind\SMB\Exception\InvalidArgumentException;
use Icewind\SMB\Exception\InvalidTypeException;
use Icewind\SMB\Exception\ConnectionAbortedException;
use Icewind\SMB\Exception\NoRouteToHostException;
use Icewind\SMB\Exception\NotEmptyException;
use Icewind\SMB\Exception\NotFoundException;
use Icewind\SMB\Exception\OutOfSpaceException;
use Icewind\SMB\Exception\TimedOutException;
use Icewind\SMB\IAuth;
use Icewind\SMB\IOptions;
/**
* Low level wrapper for libsmbclient-php with error handling
*/
class NativeState {
/** @var resource|null */
protected $state = null;
/** @var bool */
protected $handlerSet = false;
/** @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,
2 => NotFoundException::class,
13 => ForbiddenException::class,
16 => FileInUseException::class,
17 => AlreadyExistsException::class,
20 => InvalidTypeException::class,
21 => InvalidTypeException::class,
22 => InvalidArgumentException::class,
28 => OutOfSpaceException::class,
39 => NotEmptyException::class,
103 => ConnectionAbortedException::class,
104 => ConnectionResetException::class,
110 => TimedOutException::class,
111 => ConnectionRefusedException::class,
112 => HostDownException::class,
113 => NoRouteToHostException::class
];
protected function handleError(?string $path): void {
/** @var int $error */
$error = smbclient_state_errno($this->state);
if ($error === 0) {
return;
}
throw Exception::fromMap(self::EXCEPTION_MAP, $error, $path);
}
/**
* @param mixed $result
* @param string|null $uri
* @throws Exception
*/
protected function testResult($result, ?string $uri): void {
if ($result === false or $result === null) {
// smb://host/share/path
if (is_string($uri) && count(explode('/', $uri, 5)) > 4) {
list(, , , , $path) = explode('/', $uri, 5);
$path = '/' . $path;
} else {
$path = $uri;
}
$this->handleError($path);
}
}
/**
* @param IAuth $auth
* @param IOptions $options
* @return bool
*/
public function init(IAuth $auth, IOptions $options) {
if ($this->connected) {
return true;
}
/** @var resource $state */
$state = smbclient_state_new();
$this->state = $state;
/** @psalm-suppress UnusedFunctionCall */
smbclient_option_set($this->state, SMBCLIENT_OPT_AUTO_ANONYMOUS_LOGIN, false);
/** @psalm-suppress UnusedFunctionCall */
smbclient_option_set($this->state, SMBCLIENT_OPT_TIMEOUT, $options->getTimeout() * 1000);
if (function_exists('smbclient_client_protocols')) {
smbclient_client_protocols($this->state, $options->getMinProtocol(), $options->getMaxProtocol());
}
$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());
$this->testResult($result, '');
$this->connected = true;
return $result;
}
/**
* @param string $uri
* @return resource
*/
public function opendir(string $uri) {
/** @var resource $result */
$result = @smbclient_opendir($this->state, $uri);
$this->testResult($result, $uri);
return $result;
}
/**
* @param resource $dir
* @param string $path
* @return array{"type": string, "comment": string, "name": string}|false
*/
public function readdir($dir, string $path) {
/** @var array{"type": string, "comment": string, "name": string}|false $result */
$result = @smbclient_readdir($this->state, $dir);
$this->testResult($result, $path);
return $result;
}
/**
* @param resource $dir
* @param string $path
* @return bool
*/
public function closedir($dir, string $path): bool {
/** @var bool $result */
$result = smbclient_closedir($this->state, $dir);
$this->testResult($result, $path);
return $result;
}
/**
* @param string $old
* @param string $new
* @return bool
*/
public function rename(string $old, string $new): bool {
/** @var bool $result */
$result = @smbclient_rename($this->state, $old, $this->state, $new);
$this->testResult($result, $new);
return $result;
}
/**
* @param string $uri
* @return bool
*/
public function unlink(string $uri): bool {
/** @var bool $result */
$result = @smbclient_unlink($this->state, $uri);
$this->testResult($result, $uri);
return $result;
}
/**
* @param string $uri
* @param int $mask
* @return bool
*/
public function mkdir(string $uri, int $mask = 0777): bool {
/** @var bool $result */
$result = @smbclient_mkdir($this->state, $uri, $mask);
$this->testResult($result, $uri);
return $result;
}
/**
* @param string $uri
* @return bool
*/
public function rmdir(string $uri): bool {
/** @var bool $result */
$result = @smbclient_rmdir($this->state, $uri);
$this->testResult($result, $uri);
return $result;
}
/**
* @param string $uri
* @return array{"mtime": int, "size": int, "mode": int}
*/
public function stat(string $uri): array {
/** @var array{"mtime": int, "size": int, "mode": int} $result */
$result = @smbclient_stat($this->state, $uri);
$this->testResult($result, $uri);
return $result;
}
/**
* @param resource $file
* @param string $path
* @return array{"mtime": int, "size": int, "mode": int}
*/
public function fstat($file, string $path): array {
/** @var array{"mtime": int, "size": int, "mode": int} $result */
$result = @smbclient_fstat($this->state, $file);
$this->testResult($result, $path);
return $result;
}
/**
* @param string $uri
* @param string $mode
* @param int $mask
* @return resource
*/
public function open(string $uri, string $mode, int $mask = 0666) {
/** @var resource $result */
$result = @smbclient_open($this->state, $uri, $mode, $mask);
$this->testResult($result, $uri);
return $result;
}
/**
* @param string $uri
* @param int $mask
* @return resource
*/
public function create(string $uri, int $mask = 0666) {
/** @var resource $result */
$result = @smbclient_creat($this->state, $uri, $mask);
$this->testResult($result, $uri);
return $result;
}
/**
* @param resource $file
* @param int $bytes
* @param string $path
* @return string
*/
public function read($file, int $bytes, string $path): string {
/** @var string $result */
$result = @smbclient_read($this->state, $file, $bytes);
$this->testResult($result, $path);
return $result;
}
/**
* @param resource $file
* @param string $data
* @param string $path
* @param int|null $length
* @return int
*/
public function write($file, string $data, string $path, ?int $length = null): int {
/** @var int $result */
$result = @smbclient_write($this->state, $file, $data, $length);
$this->testResult($result, $path);
return $result;
}
/**
* @param resource $file
* @param int $offset
* @param int $whence SEEK_SET | SEEK_CUR | SEEK_END
* @param string|null $path
* @return int|false new file offset as measured from the start of the file on success.
*/
public function lseek($file, int $offset, int $whence = SEEK_SET, string $path = null) {
/** @var int|false $result */
$result = @smbclient_lseek($this->state, $file, $offset, $whence);
$this->testResult($result, $path);
return $result;
}
/**
* @param resource $file
* @param int $size
* @param string $path
* @return bool
*/
public function ftruncate($file, int $size, string $path): bool {
/** @var bool $result */
$result = @smbclient_ftruncate($this->state, $file, $size);
$this->testResult($result, $path);
return $result;
}
/**
* @param resource $file
* @param string $path
* @return bool
*/
public function close($file, string $path): bool {
/** @var bool $result */
$result = @smbclient_close($this->state, $file);
$this->testResult($result, $path);
return $result;
}
/**
* @param string $uri
* @param string $key
* @return string
*/
public function getxattr(string $uri, string $key) {
/** @var string $result */
$result = @smbclient_getxattr($this->state, $uri, $key);
$this->testResult($result, $uri);
return $result;
}
/**
* @param string $uri
* @param string $key
* @param string $value
* @param int $flags
* @return bool
*/
public function setxattr(string $uri, string $key, string $value, int $flags = 0) {
/** @var bool $result */
$result = @smbclient_setxattr($this->state, $uri, $key, $value, $flags);
$this->testResult($result, $uri);
return $result;
}
public function __destruct() {
if ($this->connected) {
if (smbclient_state_free($this->state) === false) {
throw new Exception("Failed to free smb state");
}
}
}
}
@@ -0,0 +1,159 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Native;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\InvalidRequestException;
use Icewind\Streams\File;
use InvalidArgumentException;
abstract class NativeStream implements File {
/**
* @var resource
* @psalm-suppress PropertyNotSetInConstructor
*/
public $context;
/**
* @var NativeState
* @psalm-suppress PropertyNotSetInConstructor
*/
protected $state;
/**
* @var resource
* @psalm-suppress PropertyNotSetInConstructor
*/
protected $handle;
/**
* @var bool
*/
protected $eof = false;
/**
* @var string
*/
protected $url = '';
/**
* Wrap a stream from libsmbclient-php into a regular php stream
*
* @param NativeState $state
* @param resource $smbStream
* @param string $mode
* @param string $url
* @param class-string<NativeStream> $class
* @return resource
*/
protected static function wrapClass(NativeState $state, $smbStream, string $mode, string $url, string $class) {
if (stream_wrapper_register('nativesmb', $class) === false) {
throw new Exception("Failed to register stream wrapper");
}
$context = stream_context_create([
'nativesmb' => [
'state' => $state,
'handle' => $smbStream,
'url' => $url
]
]);
$fh = fopen('nativesmb://', $mode, false, $context);
if (stream_wrapper_unregister('nativesmb') === false) {
throw new Exception("Failed to unregister stream wrapper");
}
return $fh;
}
public function stream_close() {
try {
return $this->state->close($this->handle, $this->url);
} catch (\Exception $e) {
return false;
}
}
public function stream_eof() {
return $this->eof;
}
public function stream_flush() {
return false;
}
public function stream_open($path, $mode, $options, &$opened_path) {
$context = stream_context_get_options($this->context);
if (!isset($context['nativesmb']) || !is_array($context['nativesmb'])) {
throw new InvalidArgumentException("context not set");
}
$state = $context['nativesmb']['state'];
if (!$state instanceof NativeState) {
throw new InvalidArgumentException("invalid context set");
}
$this->state = $state;
$handle = $context['nativesmb']['handle'];
if (!is_resource($handle)) {
throw new InvalidArgumentException("invalid context set");
}
$this->handle = $handle;
$url = $context['nativesmb']['url'];
if (!is_string($url)) {
throw new InvalidArgumentException("invalid context set");
}
$this->url = $url;
return true;
}
public function stream_read($count) {
$result = $this->state->read($this->handle, $count, $this->url);
if (strlen($result) < $count) {
$this->eof = true;
}
return $result;
}
public function stream_seek($offset, $whence = SEEK_SET) {
$this->eof = false;
try {
return $this->state->lseek($this->handle, $offset, $whence, $this->url) !== false;
} catch (InvalidRequestException $e) {
return false;
}
}
/**
* @return array{"mtime": int, "size": int, "mode": int}|false
*/
public function stream_stat() {
try {
return $this->state->stat($this->url);
} catch (Exception $e) {
return false;
}
}
public function stream_tell() {
return $this->state->lseek($this->handle, 0, SEEK_CUR, $this->url);
}
public function stream_write($data) {
return $this->state->write($this->handle, $data, $this->url);
}
public function stream_truncate($size) {
return $this->state->ftruncate($this->handle, $size, $this->url);
}
public function stream_set_option($option, $arg1, $arg2) {
return false;
}
public function stream_lock($operation) {
return false;
}
}
@@ -0,0 +1,96 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Native;
use Icewind\SMB\StringBuffer;
/**
* Stream optimized for write only usage
*/
class NativeWriteStream extends NativeStream {
const CHUNK_SIZE = 1048576; // 1MB chunks
/** @var StringBuffer */
private $writeBuffer;
/** @var int */
private $pos = 0;
public function __construct() {
$this->writeBuffer = new StringBuffer();
}
public function stream_open($path, $mode, $options, &$opened_path): bool {
return parent::stream_open($path, $mode, $options, $opened_path);
}
/**
* Wrap a stream from libsmbclient-php into a regular php stream
*
* @param NativeState $state
* @param resource $smbStream
* @param string $mode
* @param string $url
* @return resource
*/
public static function wrap(NativeState $state, $smbStream, string $mode, string $url) {
return parent::wrapClass($state, $smbStream, $mode, $url, NativeWriteStream::class);
}
public function stream_seek($offset, $whence = SEEK_SET) {
$this->flushWrite();
$result = parent::stream_seek($offset, $whence);
if ($result) {
$pos = parent::stream_tell();
if ($pos === false) {
return false;
}
$this->pos = $pos;
}
return $result;
}
private function flushWrite(): void {
parent::stream_write($this->writeBuffer->flush());
}
public function stream_write($data) {
$written = $this->writeBuffer->push($data);
$this->pos += $written;
if ($this->writeBuffer->remaining() >= self::CHUNK_SIZE) {
$this->flushWrite();
}
return $written;
}
public function stream_close() {
try {
$this->flushWrite();
$flushResult = true;
} catch (\Exception $e) {
$flushResult = false;
}
return parent::stream_close() && $flushResult;
}
public function stream_tell() {
return $this->pos;
}
public function stream_read($count) {
return false;
}
public function stream_truncate($size) {
$this->flushWrite();
$this->pos = $size;
return parent::stream_truncate($size);
}
}
@@ -0,0 +1,56 @@
<?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;
class Options implements IOptions {
/** @var int */
private $timeout = 20;
/** @var string|null */
private $minProtocol;
/** @var string|null */
private $maxProtocol;
public function getTimeout(): int {
return $this->timeout;
}
public function setTimeout(int $timeout): void {
$this->timeout = $timeout;
}
public function getMinProtocol(): ?string {
return $this->minProtocol;
}
public function setMinProtocol(?string $minProtocol): void {
$this->minProtocol = $minProtocol;
}
public function getMaxProtocol(): ?string {
return $this->maxProtocol;
}
public function setMaxProtocol(?string $maxProtocol): void {
$this->maxProtocol = $maxProtocol;
}
}
@@ -0,0 +1,85 @@
<?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\Native\NativeServer;
use Icewind\SMB\Wrapped\Server;
class ServerFactory {
const BACKENDS = [
NativeServer::class,
Server::class
];
/** @var ISystem */
private $system;
/** @var IOptions */
private $options;
/** @var ITimeZoneProvider */
private $timeZoneProvider;
/**
* ServerFactory constructor.
*
* @param IOptions|null $options
* @param ISystem|null $system
* @param ITimeZoneProvider|null $timeZoneProvider
*/
public function __construct(
IOptions $options = null,
ISystem $system = null,
ITimeZoneProvider $timeZoneProvider = null
) {
if (is_null($options)) {
$options = new Options();
}
if (is_null($system)) {
$system = new System();
}
if (is_null($timeZoneProvider)) {
$timeZoneProvider = new TimeZoneProvider($system);
}
$this->options = $options;
$this->system = $system;
$this->timeZoneProvider = $timeZoneProvider;
}
/**
* @param string $host
* @param IAuth $credentials
* @return IServer
* @throws DependencyException
*/
public function createServer(string $host, IAuth $credentials): IServer {
foreach (self::BACKENDS as $backend) {
if (call_user_func("$backend::available", $this->system)) {
return new $backend($host, $credentials, $this->system, $this->timeZoneProvider, $this->options);
}
}
throw new DependencyException('No valid backend available, ensure smbclient is in the path or php-smbclient is installed');
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 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;
class StringBuffer {
/** @var string */
private $buffer = "";
/** @var int */
private $pos = 0;
public function clear(): void {
$this->buffer = "";
$this->pos = 0;
}
public function push(string $data): int {
$this->buffer = $this->flush() . $data;
return strlen($data);
}
public function remaining(): int {
return strlen($this->buffer) - $this->pos;
}
public function read(int $count): string {
$chunk = substr($this->buffer, $this->pos, $count);
$this->pos += strlen($chunk);
return $chunk;
}
public function flush(): string {
if ($this->pos === 0) {
$remaining = $this->buffer;
} else {
$remaining = substr($this->buffer, $this->pos);
}
$this->clear();
return $remaining;
}
}
@@ -0,0 +1,76 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB;
use Icewind\SMB\Exception\Exception;
class System implements ISystem {
/** @var (string|null)[] */
private $paths = [];
/**
* Get the path to a file descriptor of the current process
*
* @param int $num the file descriptor id
* @return string
* @throws Exception
*/
public function getFD(int $num): string {
$folders = [
'/proc/self/fd',
'/dev/fd'
];
foreach ($folders as $folder) {
if (file_exists($folder)) {
return $folder . '/' . $num;
}
}
throw new Exception('Cant find file descriptor path');
}
public function getSmbclientPath(): ?string {
return $this->getBinaryPath('smbclient');
}
public function getNetPath(): ?string {
return $this->getBinaryPath('net');
}
public function getSmbcAclsPath(): ?string {
return $this->getBinaryPath('smbcacls');
}
public function getStdBufPath(): ?string {
return $this->getBinaryPath('stdbuf');
}
public function getDatePath(): ?string {
return $this->getBinaryPath('date');
}
public function libSmbclientAvailable(): bool {
return function_exists('smbclient_state_new');
}
protected function getBinaryPath(string $binary): ?string {
if (!isset($this->paths[$binary])) {
$result = null;
$output = [];
exec("which $binary 2>&1", $output, $result);
if ($result === 0 && isset($output[0])) {
$this->paths[$binary] = (string)$output[0];
} else if (is_executable("/usr/bin/$binary")) {
$this->paths[$binary] = "/usr/bin/$binary";
} else {
$this->paths[$binary] = null;
}
}
return $this->paths[$binary];
}
}
@@ -0,0 +1,54 @@
<?php
/**
* Copyright (c) 2015 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB;
class TimeZoneProvider implements ITimeZoneProvider {
/**
* @var string[]
*/
private $timeZones = [];
/**
* @var ISystem
*/
private $system;
/**
* @param ISystem $system
*/
public function __construct(ISystem $system) {
$this->system = $system;
}
public function get(string $host): string {
if (!isset($this->timeZones[$host])) {
$timeZone = null;
$net = $this->system->getNetPath();
// for local domain names we can assume same timezone
if ($net && $host && strpos($host, '.') !== false) {
$command = sprintf(
'%s time zone -S %s',
$net,
escapeshellarg($host)
);
$timeZone = exec($command);
}
if (!$timeZone) {
$date = $this->system->getDatePath();
if ($date) {
$timeZone = exec($date . " +%z");
} else {
$timeZone = date_default_timezone_get();
}
}
$this->timeZones[$host] = $timeZone;
}
return $this->timeZones[$host];
}
}
@@ -0,0 +1,116 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\Exception\AccessDeniedException;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\ConnectionRefusedException;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\Exception\NoLoginServerException;
class Connection extends RawConnection {
const DELIMITER = 'smb:';
const DELIMITER_LENGTH = 4;
/** @var Parser */
private $parser;
/**
* @param string $command
* @param Parser $parser
* @param array<string, string> $env
*/
public function __construct(string $command, Parser $parser, array $env = []) {
parent::__construct($command, $env);
$this->parser = $parser;
}
/**
* send input to smbclient
*
* @param string $input
*/
public function write(string $input) {
return parent::write($input . PHP_EOL);
}
/**
* @throws ConnectException
*/
public function clearTillPrompt(): void {
$this->write('');
do {
$promptLine = $this->readTillPrompt();
if ($promptLine === false) {
break;
}
$this->parser->checkConnectionError($promptLine);
} while (!$this->isPrompt($promptLine));
if ($this->write('') === false) {
throw new ConnectionRefusedException();
}
$this->readTillPrompt();
}
/**
* get all unprocessed output from smbclient until the next prompt
*
* @return string[]
* @throws AuthenticationException
* @throws ConnectException
* @throws ConnectionException
* @throws InvalidHostException
* @throws NoLoginServerException
* @throws AccessDeniedException
*/
public function read(): array {
if (!$this->isValid()) {
throw new ConnectionException('Connection not valid');
}
$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 substr($line, 0, self::DELIMITER_LENGTH) === self::DELIMITER;
}
/**
* @param string|bool $promptLine (optional) prompt line that might contain some info about the error
* @throws ConnectException
* @return no-return
*/
private function unknownError($promptLine = '') {
if ($promptLine) { //maybe we have some error we missed on the previous line
throw new ConnectException('Unknown error (' . $promptLine . ')');
} else {
$error = $this->readError(); // maybe something on stderr
if ($error) {
throw new ConnectException('Unknown error (stderr: ' . $error . ')');
} else {
throw new ConnectException('Unknown error');
}
}
}
public function close(bool $terminate = true): void {
if (get_resource_type($this->getInputStream()) === 'stream') {
// ignore any errors while trying to send the close command, the process might already be dead
@$this->write('close' . PHP_EOL);
}
$this->close_process($terminate);
}
}
@@ -0,0 +1,31 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
class ErrorCodes {
/**
* connection errors
*/
const LogonFailure = 'NT_STATUS_LOGON_FAILURE';
const BadHostName = 'NT_STATUS_BAD_NETWORK_NAME';
const Unsuccessful = 'NT_STATUS_UNSUCCESSFUL';
const ConnectionRefused = 'NT_STATUS_CONNECTION_REFUSED';
const NoLogonServers = 'NT_STATUS_NO_LOGON_SERVERS';
const PathNotFound = 'NT_STATUS_OBJECT_PATH_NOT_FOUND';
const NoSuchFile = 'NT_STATUS_NO_SUCH_FILE';
const ObjectNotFound = 'NT_STATUS_OBJECT_NAME_NOT_FOUND';
const NameCollision = 'NT_STATUS_OBJECT_NAME_COLLISION';
const AccessDenied = 'NT_STATUS_ACCESS_DENIED';
const DirectoryNotEmpty = 'NT_STATUS_DIRECTORY_NOT_EMPTY';
const FileIsADirectory = 'NT_STATUS_FILE_IS_A_DIRECTORY';
const NotADirectory = 'NT_STATUS_NOT_A_DIRECTORY';
const SharingViolation = 'NT_STATUS_SHARING_VIOLATION';
const InvalidParameter = 'NT_STATUS_INVALID_PARAMETER';
const RevisionMismatch = 'NT_STATUS_REVISION_MISMATCH';
}
@@ -0,0 +1,89 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\ACL;
use Icewind\SMB\IFileInfo;
class FileInfo implements IFileInfo {
/** @var string */
protected $path;
/** @var string */
protected $name;
/** @var int */
protected $size;
/** @var int */
protected $time;
/** @var int */
protected $mode;
/** @var callable(): ACL[] */
protected $aclCallback;
/**
* @param string $path
* @param string $name
* @param int $size
* @param int $time
* @param int $mode
* @param callable(): ACL[] $aclCallback
*/
public function __construct(string $path, string $name, int $size, int $time, int $mode, callable $aclCallback) {
$this->path = $path;
$this->name = $name;
$this->size = $size;
$this->time = $time;
$this->mode = $mode;
$this->aclCallback = $aclCallback;
}
/**
* @return string
*/
public function getPath(): string {
return $this->path;
}
public function getName(): string {
return $this->name;
}
public function getSize(): int {
return $this->size;
}
public function getMTime(): int {
return $this->time;
}
public function isDirectory(): bool {
return (bool)($this->mode & IFileInfo::MODE_DIRECTORY);
}
public function isReadOnly(): bool {
return (bool)($this->mode & IFileInfo::MODE_READONLY);
}
public function isHidden(): bool {
return (bool)($this->mode & IFileInfo::MODE_HIDDEN);
}
public function isSystem(): bool {
return (bool)($this->mode & IFileInfo::MODE_SYSTEM);
}
public function isArchived(): bool {
return (bool)($this->mode & IFileInfo::MODE_ARCHIVE);
}
/**
* @return ACL[]
*/
public function getAcls(): array {
return ($this->aclCallback)();
}
}
@@ -0,0 +1,113 @@
<?php
/**
* @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\Change;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\RevisionMismatchException;
use Icewind\SMB\INotifyHandler;
class NotifyHandler implements INotifyHandler {
/** @var Connection */
private $connection;
/** @var string */
private $path;
/** @var bool */
private $listening = true;
// see error.h
const EXCEPTION_MAP = [
ErrorCodes::RevisionMismatch => RevisionMismatchException::class,
];
/**
* @param Connection $connection
* @param string $path
*/
public function __construct(Connection $connection, string $path) {
$this->connection = $connection;
$this->path = $path;
}
/**
* Get all changes detected since the start of the notify process or the last call to getChanges
*
* @return Change[]
*/
public function getChanges(): array {
if (!$this->listening) {
return [];
}
stream_set_blocking($this->connection->getOutputStream(), false);
$lines = [];
while (($line = $this->connection->readLine())) {
$this->checkForError($line);
$lines[] = $line;
}
stream_set_blocking($this->connection->getOutputStream(), true);
return array_values(array_filter(array_map([$this, 'parseChangeLine'], $lines)));
}
/**
* Listen actively to all incoming changes
*
* Note that this is a blocking process and will cause the process to block forever if not explicitly terminated
*
* @param callable(Change):?bool $callback
*/
public function listen(callable $callback): void {
if ($this->listening) {
while (true) {
$line = $this->connection->readLine();
if ($line === false) {
break;
}
$this->checkForError($line);
$change = $this->parseChangeLine($line);
if ($change) {
$result = $callback($change);
if ($result === false) {
break;
}
}
};
}
}
private function parseChangeLine(string $line): ?Change {
$code = (int)substr($line, 0, 4);
if ($code === 0) {
return null;
}
$subPath = str_replace('\\', '/', substr($line, 5));
if ($this->path === '') {
return new Change($code, $subPath);
} else {
return new Change($code, $this->path . '/' . $subPath);
}
}
private function checkForError(string $line): void {
if (substr($line, 0, 16) === 'notify returned ') {
$error = substr($line, 16);
throw Exception::fromMap(array_merge(self::EXCEPTION_MAP, Parser::EXCEPTION_MAP), $error, 'Notify is not supported with the used smb version');
}
}
public function stop(): void {
$this->listening = false;
$this->connection->close();
}
public function __destruct() {
$this->stop();
}
}
@@ -0,0 +1,277 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\ACL;
use Icewind\SMB\Exception\AccessDeniedException;
use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\FileInUseException;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\Exception\InvalidParameterException;
use Icewind\SMB\Exception\InvalidResourceException;
use Icewind\SMB\Exception\InvalidTypeException;
use Icewind\SMB\Exception\NoLoginServerException;
use Icewind\SMB\Exception\NotEmptyException;
use Icewind\SMB\Exception\NotFoundException;
class Parser {
const MSG_NOT_FOUND = 'Error opening local file ';
/**
* @var string
*/
protected $timeZone;
// see error.h
const EXCEPTION_MAP = [
ErrorCodes::LogonFailure => AuthenticationException::class,
ErrorCodes::PathNotFound => NotFoundException::class,
ErrorCodes::ObjectNotFound => NotFoundException::class,
ErrorCodes::NoSuchFile => NotFoundException::class,
ErrorCodes::NameCollision => AlreadyExistsException::class,
ErrorCodes::AccessDenied => AccessDeniedException::class,
ErrorCodes::DirectoryNotEmpty => NotEmptyException::class,
ErrorCodes::FileIsADirectory => InvalidTypeException::class,
ErrorCodes::NotADirectory => InvalidTypeException::class,
ErrorCodes::SharingViolation => FileInUseException::class,
ErrorCodes::InvalidParameter => InvalidParameterException::class
];
const MODE_STRINGS = [
'R' => FileInfo::MODE_READONLY,
'H' => FileInfo::MODE_HIDDEN,
'S' => FileInfo::MODE_SYSTEM,
'D' => FileInfo::MODE_DIRECTORY,
'A' => FileInfo::MODE_ARCHIVE,
'N' => FileInfo::MODE_NORMAL
];
/**
* @param string $timeZone
*/
public function __construct(string $timeZone) {
$this->timeZone = $timeZone;
}
private function getErrorCode(string $line): ?string {
$parts = explode(' ', $line);
foreach ($parts as $part) {
if (substr($part, 0, 9) === 'NT_STATUS') {
return $part;
}
}
return null;
}
/**
* @param string[] $output
* @param string $path
* @return no-return
* @throws Exception
* @throws InvalidResourceException
* @throws NotFoundException
*/
public function checkForError(array $output, string $path): void {
if (strpos($output[0], 'does not exist')) {
throw new NotFoundException($path);
}
$error = $this->getErrorCode($output[0]);
if (substr($output[0], 0, strlen(self::MSG_NOT_FOUND)) === self::MSG_NOT_FOUND) {
$localPath = substr($output[0], strlen(self::MSG_NOT_FOUND));
throw new InvalidResourceException('Failed opening local file "' . $localPath . '" for writing');
}
throw Exception::fromMap(self::EXCEPTION_MAP, $error, $path);
}
/**
* check if the first line holds a connection failure
*
* @param string $line
* @throws AuthenticationException
* @throws InvalidHostException
* @throws NoLoginServerException
* @throws AccessDeniedException
*/
public function checkConnectionError(string $line): void {
$line = rtrim($line, ')');
if (substr($line, -23) === ErrorCodes::LogonFailure) {
throw new AuthenticationException('Invalid login');
}
if (substr($line, -26) === ErrorCodes::BadHostName) {
throw new InvalidHostException('Invalid hostname');
}
if (substr($line, -22) === ErrorCodes::Unsuccessful) {
throw new InvalidHostException('Connection unsuccessful');
}
if (substr($line, -28) === ErrorCodes::ConnectionRefused) {
throw new InvalidHostException('Connection refused');
}
if (substr($line, -26) === ErrorCodes::NoLogonServers) {
throw new NoLoginServerException('No login server');
}
if (substr($line, -23) === ErrorCodes::AccessDenied) {
throw new AccessDeniedException('Access denied');
}
}
public function parseMode(string $mode): int {
$result = 0;
foreach (self::MODE_STRINGS as $char => $val) {
if (strpos($mode, $char) !== false) {
$result |= $val;
}
}
return $result;
}
/**
* @param string[] $output
* @return array{"mtime": int, "mode": int, "size": int}
* @throws Exception
*/
public function parseStat(array $output): array {
$data = [];
foreach ($output as $line) {
// A line = explode statement may not fill all array elements
// properly. May happen when accessing non Windows Fileservers
$words = explode(':', $line, 2);
$name = $words[0] ?? '';
$value = $words[1] ?? '';
$value = trim($value);
if (!isset($data[$name])) {
$data[$name] = $value;
}
}
$attributeStart = strpos($data['attributes'], '(');
if ($attributeStart === false) {
throw new Exception("Malformed state response from server");
}
return [
'mtime' => strtotime($data['write_time']),
'mode' => hexdec(substr($data['attributes'], $attributeStart + 1, -1)),
'size' => isset($data['stream']) ? (int)(explode(' ', $data['stream'])[1]) : 0
];
}
/**
* @param string[] $output
* @param string $basePath
* @param callable(string):ACL[] $aclCallback
* @return FileInfo[]
*/
public function parseDir(array $output, string $basePath, callable $aclCallback): array {
//last line is used space
array_pop($output);
$regex = '/^\s*(.*?)\s\s\s\s+(?:([NDHARSCndharsc]*)\s+)?([0-9]+)\s+(.*)$/';
//2 spaces, filename, optional type, size, date
$content = [];
foreach ($output as $line) {
if (preg_match($regex, $line, $matches)) {
list(, $name, $mode, $size, $time) = $matches;
if ($name !== '.' and $name !== '..') {
$mode = $this->parseMode(strtoupper($mode));
$time = strtotime($time . ' ' . $this->timeZone);
$path = $basePath . '/' . $name;
$content[] = new FileInfo($path, $name, (int)$size, $time, $mode, function () use ($aclCallback, $path): array {
return $aclCallback($path);
});
}
}
}
return $content;
}
/**
* @param string[] $output
* @return array<string, string>
*/
public function parseListShares(array $output): array {
$shareNames = [];
foreach ($output as $line) {
if (strpos($line, '|')) {
list($type, $name, $description) = explode('|', $line);
if (strtolower($type) === 'disk') {
$shareNames[$name] = $description;
}
} elseif (strpos($line, 'Disk')) {
// new output format
list($name, $description) = explode('Disk', $line);
$shareNames[trim($name)] = trim($description);
}
}
return $shareNames;
}
/**
* @param string[] $rawAcls
* @return ACL[]
*/
public function parseACLs(array $rawAcls): array {
$acls = [];
foreach ($rawAcls as $acl) {
if (strpos($acl, ':') === false) {
continue;
}
[$type, $acl] = explode(':', $acl, 2);
if ($type !== 'ACL') {
continue;
}
[$user, $permissions] = explode(':', $acl, 2);
[$type, $flags, $mask] = explode('/', $permissions);
$type = $type === 'ALLOWED' ? ACL::TYPE_ALLOW : ACL::TYPE_DENY;
$flagsInt = 0;
foreach (explode('|', $flags) as $flagString) {
if ($flagString === 'OI') {
$flagsInt += ACL::FLAG_OBJECT_INHERIT;
} elseif ($flagString === 'CI') {
$flagsInt += ACL::FLAG_CONTAINER_INHERIT;
}
}
if (substr($mask, 0, 2) === '0x') {
$maskInt = hexdec($mask);
} else {
$maskInt = 0;
foreach (explode('|', $mask) as $maskString) {
if ($maskString === 'R') {
$maskInt += ACL::MASK_READ;
} elseif ($maskString === 'W') {
$maskInt += ACL::MASK_WRITE;
} elseif ($maskString === 'X') {
$maskInt += ACL::MASK_EXECUTE;
} elseif ($maskString === 'D') {
$maskInt += ACL::MASK_DELETE;
} elseif ($maskString === 'READ') {
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE;
} elseif ($maskString === 'CHANGE') {
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE;
} elseif ($maskString === 'FULL') {
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE;
}
}
}
if (isset($acls[$user])) {
$existing = $acls[$user];
$maskInt += $existing->getMask();
}
$acls[$user] = new ACL($type, $flagsInt, $maskInt);
}
ksort($acls);
return $acls;
}
}
@@ -0,0 +1,250 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\ConnectionException;
class RawConnection {
/**
* @var string
*/
private $command;
/**
* @var string[]
*/
private $env;
/**
* @var resource[] $pipes
*
* $pipes[0] holds STDIN for smbclient
* $pipes[1] holds STDOUT for smbclient
* $pipes[3] holds the authfile for smbclient
* $pipes[4] holds the stream for writing files
* $pipes[5] holds the stream for reading files
*/
private $pipes = [];
/**
* @var resource|null $process
*/
private $process;
/**
* @var resource|null $authStream
*/
private $authStream = null;
/**
* @param string $command
* @param array<string, string> $env
*/
public function __construct(string $command, array $env = []) {
$this->command = $command;
$this->env = $env;
}
/**
* @throws ConnectException
* @psalm-assert resource $this->process
*/
public function connect(): void {
if (is_null($this->getAuthStream())) {
throw new ConnectException('Authentication not set before connecting');
}
$descriptorSpec = [
0 => ['pipe', 'r'], // child reads from stdin
1 => ['pipe', 'w'], // child writes to stdout
2 => ['pipe', 'w'], // child writes to stderr
3 => $this->getAuthStream(), // child reads from fd#3
4 => ['pipe', 'r'], // child reads from fd#4
5 => ['pipe', 'w'] // child writes to fd#5
];
setlocale(LC_ALL, Server::LOCALE);
$env = array_merge($this->env, [
'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
'TZ' => 'UTC',
]);
$this->process = proc_open($this->command, $descriptorSpec, $this->pipes, '/', $env);
if (!$this->isValid()) {
throw new ConnectionException();
}
}
/**
* check if the connection is still active
*
* @return bool
* @psalm-assert-if-true resource $this->process
*/
public function isValid(): bool {
if (is_resource($this->process)) {
$status = proc_get_status($this->process);
return $status['running'];
} else {
return false;
}
}
/**
* send input to the process
*
* @param string $input
* @return int|bool
*/
public function write(string $input) {
$result = @fwrite($this->getInputStream(), $input);
fflush($this->getInputStream());
return $result;
}
/**
* 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(string $end = "\n") {
return stream_get_line($this->getOutputStream(), 4096, $end);
}
/**
* read a line of output
*
* @return string|false
*/
public function readError() {
$line = stream_get_line($this->getErrorStream(), 4086);
return $line !== false ? trim($line) : false;
}
/**
* get all output until the process closes
*
* @return string[]
*/
public function readAll(): array {
$output = [];
while ($line = $this->readLine()) {
$output[] = $line;
}
return $output;
}
/**
* @return resource
*/
public function getInputStream() {
return $this->pipes[0];
}
/**
* @return resource
*/
public function getOutputStream() {
return $this->pipes[1];
}
/**
* @return resource
*/
public function getErrorStream() {
return $this->pipes[2];
}
/**
* @return resource|null
*/
public function getAuthStream() {
return $this->authStream;
}
/**
* @return resource
*/
public function getFileInputStream() {
return $this->pipes[4];
}
/**
* @return resource
*/
public function getFileOutputStream() {
return $this->pipes[5];
}
/**
* @param string|null $user
* @param string|null $password
* @psalm-assert resource $this->authStream
*/
public function writeAuthentication(?string $user, ?string $password): void {
$auth = ($password === null)
? "username=$user"
: "username=$user\npassword=$password\n";
$this->authStream = fopen('php://temp', 'w+');
fwrite($this->authStream, $auth);
}
/**
* @param bool $terminate
* @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;
}
if ($terminate) {
proc_terminate($this->process);
}
proc_close($this->process);
$this->process = null;
}
public function reconnect(): void {
$this->close();
$this->connect();
}
public function __destruct() {
$this->close();
}
}
@@ -0,0 +1,104 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\AbstractServer;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\ConnectionRefusedException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\IShare;
use Icewind\SMB\ISystem;
class Server extends AbstractServer {
/**
* Check if the smbclient php extension is available
*
* @param ISystem $system
* @return bool
*/
public static function available(ISystem $system): bool {
return $system->getSmbclientPath() !== null;
}
private function getAuthFileArgument(): string {
if ($this->getAuth()->getUsername()) {
return '--authentication-file=' . $this->system->getFD(3);
} else {
return '';
}
}
/**
* @return IShare[]
*
* @throws AuthenticationException
* @throws InvalidHostException
* @throws ConnectException
*/
public function listShares(): array {
$maxProtocol = $this->options->getMaxProtocol();
$minProtocol = $this->options->getMinProtocol();
$smbClient = $this->system->getSmbclientPath();
if ($smbClient === null) {
throw new Exception("Backend not available");
}
$command = sprintf(
'%s %s %s %s %s -L %s',
$smbClient,
$this->getAuthFileArgument(),
$this->getAuth()->getExtraCommandLineArguments(),
$maxProtocol ? "--option='client max protocol=" . $maxProtocol . "'" : "",
$minProtocol ? "--option='client min protocol=" . $minProtocol . "'" : "",
escapeshellarg('//' . $this->getHost())
);
$connection = new RawConnection($command);
$connection->writeAuthentication($this->getAuth()->getUsername(), $this->getAuth()->getPassword());
$connection->connect();
if (!$connection->isValid()) {
throw new ConnectionException((string)$connection->readLine());
}
$parser = new Parser('UTC');
$output = $connection->readAll();
if (isset($output[0])) {
$parser->checkConnectionError($output[0]);
}
// sometimes we get an empty line first
if (count($output) < 2) {
$output = $connection->readAll();
}
if (isset($output[0])) {
$parser->checkConnectionError($output[0]);
}
if (count($output) === 0) {
throw new ConnectionRefusedException();
}
$shareNames = $parser->parseListShares($output);
$shares = [];
foreach ($shareNames as $name => $_description) {
$shares[] = $this->getShare($name);
}
return $shares;
}
/**
* @param string $name
* @return IShare
*/
public function getShare(string $name): IShare {
return new Share($this, $name, $this->system);
}
}
@@ -0,0 +1,554 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\AbstractShare;
use Icewind\SMB\ACL;
use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\DependencyException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\FileInUseException;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\Exception\InvalidTypeException;
use Icewind\SMB\Exception\NotFoundException;
use Icewind\SMB\Exception\InvalidRequestException;
use Icewind\SMB\IFileInfo;
use Icewind\SMB\INotifyHandler;
use Icewind\SMB\IServer;
use Icewind\SMB\ISystem;
use Icewind\Streams\CallbackWrapper;
use Icewind\SMB\Native\NativeShare;
use Icewind\SMB\Native\NativeServer;
class Share extends AbstractShare {
/**
* @var IServer $server
*/
private $server;
/**
* @var string $name
*/
private $name;
/**
* @var Connection|null $connection
*/
public $connection = null;
/**
* @var Parser
*/
protected $parser;
/**
* @var ISystem
*/
private $system;
const MODE_MAP = [
FileInfo::MODE_READONLY => 'r',
FileInfo::MODE_HIDDEN => 'h',
FileInfo::MODE_ARCHIVE => 'a',
FileInfo::MODE_SYSTEM => 's'
];
const EXEC_CMD = 'exec';
/**
* @param IServer $server
* @param string $name
* @param ISystem $system
*/
public function __construct(IServer $server, string $name, ISystem $system) {
parent::__construct();
$this->server = $server;
$this->name = $name;
$this->system = $system;
$this->parser = new Parser('UTC');
}
private function getAuthFileArgument(): string {
if ($this->server->getAuth()->getUsername()) {
return '--authentication-file=' . $this->system->getFD(3);
} else {
return '';
}
}
protected function getConnection(): Connection {
$maxProtocol = $this->server->getOptions()->getMaxProtocol();
$minProtocol = $this->server->getOptions()->getMinProtocol();
$smbClient = $this->system->getSmbclientPath();
$stdBuf = $this->system->getStdBufPath();
if ($smbClient === null) {
throw new Exception("Backend not available");
}
$command = sprintf(
'%s %s%s -t %s %s %s %s %s %s',
self::EXEC_CMD,
$stdBuf ? $stdBuf . ' -o0 ' : '',
$smbClient,
$this->server->getOptions()->getTimeout(),
$this->getAuthFileArgument(),
$this->server->getAuth()->getExtraCommandLineArguments(),
$maxProtocol ? "--option='client max protocol=" . $maxProtocol . "'" : "",
$minProtocol ? "--option='client min protocol=" . $minProtocol . "'" : "",
escapeshellarg('//' . $this->server->getHost() . '/' . $this->name)
);
$connection = new Connection($command, $this->parser);
$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
$connection->connect();
if (!$connection->isValid()) {
throw new ConnectionException((string)$connection->readLine());
}
// some versions of smbclient add a help message in first of the first prompt
$connection->clearTillPrompt();
return $connection;
}
/**
* @throws ConnectionException
* @throws AuthenticationException
* @throws InvalidHostException
* @psalm-assert Connection $this->connection
*/
protected function connect(): Connection {
if ($this->connection and $this->connection->isValid()) {
return $this->connection;
}
$this->connection = $this->getConnection();
return $this->connection;
}
/**
* @throws ConnectionException
* @throws AuthenticationException
* @throws InvalidHostException
* @psalm-assert Connection $this->connection
*/
protected function reconnect(): void {
if ($this->connection === null) {
$this->connect();
} else {
$this->connection->reconnect();
if (!$this->connection->isValid()) {
throw new ConnectionException();
}
}
}
/**
* Get the name of the share
*
* @return string
*/
public function getName(): string {
return $this->name;
}
protected function simpleCommand(string $command, string $path): bool {
$escapedPath = $this->escapePath($path);
$cmd = $command . ' ' . $escapedPath;
$output = $this->execute($cmd);
return $this->parseOutput($output, $path);
}
/**
* List the content of a remote folder
*
* @param string $path
* @return IFileInfo[]
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function dir(string $path): array {
$escapedPath = $this->escapePath($path);
$output = $this->execute('cd ' . $escapedPath);
//check output for errors
$this->parseOutput($output, $path);
$output = $this->execute('dir');
$this->execute('cd /');
return $this->parser->parseDir($output, $path, function (string $path) {
return $this->getAcls($path);
});
}
/**
* @param string $path
* @return IFileInfo
*/
public function stat(string $path): IFileInfo {
// some windows server setups don't seem to like the allinfo command
// use the dir command instead to get the file info where possible
if ($path !== "" && $path !== "/") {
$parent = dirname($path);
$dir = $this->dir($parent);
$file = array_values(array_filter($dir, function (IFileInfo $info) use ($path) {
return $info->getPath() === $path;
}));
if ($file) {
return $file[0];
}
}
$escapedPath = $this->escapePath($path);
$output = $this->execute('allinfo ' . $escapedPath);
// Windows and non Windows Fileserver may respond different
// to the allinfo command for directories. If the result is a single
// line = error line, redo it with a different allinfo parameter
if ($escapedPath == '""' && count($output) < 2) {
$output = $this->execute('allinfo ' . '"."');
}
if (count($output) < 3) {
$this->parseOutput($output, $path);
}
$stat = $this->parser->parseStat($output);
return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], $stat['mode'], function () use ($path) {
return $this->getAcls($path);
});
}
/**
* Create a folder on the share
*
* @param string $path
* @return bool
*
* @throws NotFoundException
* @throws AlreadyExistsException
*/
public function mkdir(string $path): bool {
return $this->simpleCommand('mkdir', $path);
}
/**
* Remove a folder on the share
*
* @param string $path
* @return bool
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function rmdir(string $path): bool {
return $this->simpleCommand('rmdir', $path);
}
/**
* Delete a file on the share
*
* @param string $path
* @param bool $secondTry
* @return bool
* @throws InvalidTypeException
* @throws NotFoundException
* @throws \Exception
*/
public function del(string $path, bool $secondTry = false): bool {
//del return a file not found error when trying to delete a folder
//we catch it so we can check if $path doesn't exist or is of invalid type
try {
return $this->simpleCommand('del', $path);
} catch (NotFoundException $e) {
//no need to do anything with the result, we just check if this throws the not found error
try {
$this->simpleCommand('ls', $path);
} catch (NotFoundException $e2) {
throw $e;
} catch (\Exception $e2) {
throw new InvalidTypeException($path);
}
throw $e;
} catch (FileInUseException $e) {
if ($secondTry) {
throw $e;
}
$this->reconnect();
return $this->del($path, true);
}
}
/**
* Rename a remote file
*
* @param string $from
* @param string $to
* @return bool
*
* @throws NotFoundException
* @throws AlreadyExistsException
*/
public function rename(string $from, string $to): bool {
$path1 = $this->escapePath($from);
$path2 = $this->escapePath($to);
$output = $this->execute('rename ' . $path1 . ' ' . $path2);
return $this->parseOutput($output, $to);
}
/**
* Upload a local file
*
* @param string $source local file
* @param string $target remove file
* @return bool
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function put(string $source, string $target): bool {
$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
$path2 = $this->escapePath($target);
$output = $this->execute('put ' . $path1 . ' ' . $path2);
return $this->parseOutput($output, $target);
}
/**
* Download a remote file
*
* @param string $source remove file
* @param string $target local file
* @return bool
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function get(string $source, string $target): bool {
$path1 = $this->escapePath($source);
$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping
$output = $this->execute('get ' . $path1 . ' ' . $path2);
return $this->parseOutput($output, $source);
}
/**
* Open a readable stream to a remote file
*
* @param string $source
* @return resource a read only stream with the contents of the remote file
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function read(string $source) {
$source = $this->escapePath($source);
// 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();
$fh = CallbackWrapper::wrap($fh, function() use ($connection) {
$connection->write('');
});
if (!is_resource($fh)) {
throw new Exception("Failed to wrap file output");
}
return $fh;
}
/**
* Open a writable stream to a remote file
*
* @param string $target
* @return resource a write only stream to upload a remote file
*
* @throws NotFoundException
* @throws InvalidTypeException
*/
public function write(string $target) {
$target = $this->escapePath($target);
// 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();
$fh = $connection->getFileInputStream();
$connection->write('put ' . $this->system->getFD(4) . ' ' . $target);
$connection->write('exit');
// 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, function() use ($connection) {
$connection->write('');
}, null, function () use ($connection) {
$connection->close(false); // dont terminate, give the upload some time
});
if (is_resource($stream)) {
return $stream;
} else {
throw new InvalidRequestException($target);
}
}
/**
* Append to stream
* Note: smbclient does not support this (Use php-libsmbclient)
*
* @param string $target
*
* @throws DependencyException
*/
public function append(string $target) {
throw new DependencyException('php-libsmbclient is required for append');
}
/**
* @param string $path
* @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
* @return mixed
*/
public function setMode(string $path, int $mode) {
$modeString = '';
foreach (self::MODE_MAP as $modeByte => $string) {
if ($mode & $modeByte) {
$modeString .= $string;
}
}
$path = $this->escapePath($path);
// first reset the mode to normal
$cmd = 'setmode ' . $path . ' -rsha';
$output = $this->execute($cmd);
$this->parseOutput($output, $path);
if ($mode !== FileInfo::MODE_NORMAL) {
// then set the modes we want
$cmd = 'setmode ' . $path . ' ' . $modeString;
$output = $this->execute($cmd);
return $this->parseOutput($output, $path);
} else {
return true;
}
}
/**
* @param string $path
* @return INotifyHandler
* @throws ConnectionException
* @throws DependencyException
*/
public function notify(string $path): INotifyHandler {
if (!$this->system->getStdBufPath()) { //stdbuf is required to disable smbclient's output buffering
throw new DependencyException('stdbuf is required for usage of the notify command');
}
$connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process
$command = 'notify ' . $this->escapePath($path);
$connection->write($command . PHP_EOL);
return new NotifyHandler($connection, $path);
}
/**
* @param string $command
* @return string[]
*/
protected function execute(string $command): array {
$this->connect()->write($command);
return $this->connect()->read();
}
/**
* check output for errors
*
* @param string[] $lines
* @param string $path
*
* @return bool
* @throws AlreadyExistsException
* @throws \Icewind\SMB\Exception\AccessDeniedException
* @throws \Icewind\SMB\Exception\NotEmptyException
* @throws InvalidTypeException
* @throws \Icewind\SMB\Exception\Exception
* @throws NotFoundException
*/
protected function parseOutput(array $lines, string $path = ''): bool {
if (count($lines) === 0) {
return true;
} else {
$this->parser->checkForError($lines, $path);
}
}
/**
* @param string $string
* @return string
*/
protected function escape(string $string): string {
return escapeshellarg($string);
}
/**
* @param string $path
* @return string
*/
protected function escapePath(string $path): string {
$this->verifyPath($path);
if ($path === '/') {
$path = '';
}
$path = str_replace('/', '\\', $path);
$path = str_replace('"', '^"', $path);
$path = ltrim($path, '\\');
return '"' . $path . '"';
}
/**
* @param string $path
* @return string
*/
protected function escapeLocalPath(string $path): string {
$path = str_replace('"', '\"', $path);
return '"' . $path . '"';
}
/**
* @param string $path
* @return ACL[]
* @throws ConnectionException
* @throws ConnectException
*/
protected function getAcls(string $path): array {
$commandPath = $this->system->getSmbcAclsPath();
if (!$commandPath) {
return [];
}
$command = sprintf(
'%s %s %s %s/%s %s',
$commandPath,
$this->getAuthFileArgument(),
$this->server->getAuth()->getExtraCommandLineArguments(),
escapeshellarg('//' . $this->server->getHost()),
escapeshellarg($this->name),
escapeshellarg($path)
);
$connection = new RawConnection($command);
$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
$connection->connect();
if (!$connection->isValid()) {
throw new ConnectionException((string)$connection->readLine());
}
$rawAcls = $connection->readAll();
return $this->parser->parseACLs($rawAcls);
}
public function getServer(): IServer {
return $this->server;
}
public function __destruct() {
unset($this->connection);
}
}