Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
'OCA\\Files_External\\Lib\\MissingDependency' => $baseDir . '/../lib/Lib/MissingDependency.php',
'OCA\\Files_External\\Lib\\Notify\\SMBNotifyHandler' => $baseDir . '/../lib/Lib/Notify/SMBNotifyHandler.php',
'OCA\\Files_External\\Lib\\PersonalMount' => $baseDir . '/../lib/Lib/PersonalMount.php',
'OCA\\Files_External\\Lib\\PortHelper' => $baseDir . '/../lib/Lib/PortHelper.php',
'OCA\\Files_External\\Lib\\PriorityTrait' => $baseDir . '/../lib/Lib/PriorityTrait.php',
'OCA\\Files_External\\Lib\\SessionStorageWrapper' => $baseDir . '/../lib/Lib/SessionStorageWrapper.php',
'OCA\\Files_External\\Lib\\StorageConfig' => $baseDir . '/../lib/Lib/StorageConfig.php',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class ComposerStaticInitFiles_External
'OCA\\Files_External\\Lib\\MissingDependency' => __DIR__ . '/..' . '/../lib/Lib/MissingDependency.php',
'OCA\\Files_External\\Lib\\Notify\\SMBNotifyHandler' => __DIR__ . '/..' . '/../lib/Lib/Notify/SMBNotifyHandler.php',
'OCA\\Files_External\\Lib\\PersonalMount' => __DIR__ . '/..' . '/../lib/Lib/PersonalMount.php',
'OCA\\Files_External\\Lib\\PortHelper' => __DIR__ . '/..' . '/../lib/Lib/PortHelper.php',
'OCA\\Files_External\\Lib\\PriorityTrait' => __DIR__ . '/..' . '/../lib/Lib/PriorityTrait.php',
'OCA\\Files_External\\Lib\\SessionStorageWrapper' => __DIR__ . '/..' . '/../lib/Lib/SessionStorageWrapper.php',
'OCA\\Files_External\\Lib\\StorageConfig' => __DIR__ . '/..' . '/../lib/Lib/StorageConfig.php',
Expand Down
48 changes: 48 additions & 0 deletions apps/files_external/lib/Lib/PortHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files_External\Lib;

/**
* Helper to turn a configured port into a usable TCP port number.
*
* The external storage settings store whatever the admin typed into the port
* field, so the value can be missing, an empty string, a non-numeric string or
* a number outside of the valid TCP port range.
*/
final class PortHelper {
/** Lowest valid TCP port */
public const MIN_PORT = 1;

/** Highest valid TCP port */
public const MAX_PORT = 65535;

/**
* Parse a configured port value
*
* @param mixed $port the configured value, may be of any type
* @param int $fallback port to use when the configured value is not a valid TCP port
* @return int the configured port, or $fallback if it is not an integer within the valid TCP port range
*/
public static function parsePort(mixed $port, int $fallback): int {
if (is_int($port)) {
$parsedPort = $port;
} elseif (is_string($port) && preg_match('/^\d+$/', $port) === 1) {
$parsedPort = (int)$port;
} else {
return $fallback;
}

if ($parsedPort < self::MIN_PORT || $parsedPort > self::MAX_PORT) {
return $fallback;
}

return $parsedPort;
}
}
6 changes: 4 additions & 2 deletions apps/files_external/lib/Lib/Storage/FTP.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Icewind\Streams\IteratorDirectory;
use OC\Files\Storage\Common;
use OC\Files\Storage\PolyFill\CopyDirectory;
use OCA\Files_External\Lib\PortHelper;
use OCP\Constants;
use OCP\Files\FileInfo;
use OCP\Files\IMimeTypeDetector;
Expand All @@ -23,6 +24,8 @@
class FTP extends Common {
use CopyDirectory;

private const DEFAULT_PORT = 21;

private $root;
private $host;
private $password;
Expand All @@ -49,8 +52,7 @@ public function __construct(array $parameters) {
$this->secure = false;
}
$this->root = isset($parameters['root']) ? '/' . ltrim($parameters['root']) : '/';
$parsedPort = $parameters['port'] ?? null;
$this->port = is_numeric($parsedPort) ? (int)$parsedPort : 21;
$this->port = PortHelper::parsePort($parameters['port'] ?? null, self::DEFAULT_PORT);
$this->utf8Mode = isset($parameters['utf8']) && $parameters['utf8'];
} else {
throw new \Exception('Creating ' . self::class . ' storage failed, required parameters not set');
Expand Down
18 changes: 10 additions & 8 deletions apps/files_external/lib/Lib/Storage/SFTP.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Icewind\Streams\RetryWrapper;
use OC\Files\Storage\Common;
use OC\Files\View;
use OCA\Files_External\Lib\PortHelper;
use OCP\Cache\CappedMemoryCache;
use OCP\Constants;
use OCP\Files\FileInfo;
Expand All @@ -26,10 +27,12 @@
* provide access to SFTP servers.
*/
class SFTP extends Common {
private const DEFAULT_PORT = 22;

private $host;
private $user;
private $root;
private $port = 22;
private $port = self::DEFAULT_PORT;

private $auth = [];

Expand All @@ -56,11 +59,11 @@ private function splitHost(string $host): array {

$parsed = parse_url($host);
if (is_array($parsed) && isset($parsed['port'])) {
return [$parsed['host'], $parsed['port']];
return [$parsed['host'], PortHelper::parsePort($parsed['port'], self::DEFAULT_PORT)];
} elseif (is_array($parsed)) {
return [$parsed['host'], 22];
return [$parsed['host'], self::DEFAULT_PORT];
} else {
return [$input, 22];
return [$input, self::DEFAULT_PORT];
}
}

Expand All @@ -78,10 +81,9 @@ public function __construct(array $parameters) {
$parsedHost = $this->splitHost($parameters['host']);
$this->host = $parsedHost[0];

// Handle empty port parameter to allow host-defined ports
// and ensure strictly numeric ports
$parsedPort = $parameters['port'] ?? null;
$this->port = (int)(is_numeric($parsedPort) ? $parsedPort : $parsedHost[1]);
// Fall back to the port from the host field, and to the default port,
// unless a valid port is configured
$this->port = PortHelper::parsePort($parameters['port'] ?? null, $parsedHost[1]);

if (!isset($parameters['user'])) {
throw new \UnexpectedValueException('no authentication parameters specified');
Expand Down
5 changes: 5 additions & 0 deletions apps/files_external/tests/FtpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public static function portProvider(): array {
'non numeric port' => [array_merge($parameters, ['port' => 'ftp']), 21],
'numeric string port' => [array_merge($parameters, ['port' => '2121']), 2121],
'integer port' => [array_merge($parameters, ['port' => 2121]), 2121],
'decimal port' => [array_merge($parameters, ['port' => '21.5']), 21],
'zero port' => [array_merge($parameters, ['port' => '0']), 21],
'negative port' => [array_merge($parameters, ['port' => '-2121']), 21],
'out of range port' => [array_merge($parameters, ['port' => '65536']), 21],
'highest valid port' => [array_merge($parameters, ['port' => '65535']), 65535],
];
}

Expand Down
63 changes: 63 additions & 0 deletions apps/files_external/tests/PortHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files_External\Tests;

use OCA\Files_External\Lib\PortHelper;
use PHPUnit\Framework\Attributes\DataProvider;
use Test\TestCase;

class PortHelperTest extends TestCase {
public static function portProvider(): array {
return [
// valid ports are returned as integers
'integer port' => [2121, 2121],
'numeric string port' => ['2121', 2121],
'padded numeric string port' => ['0022', 22],
'lowest valid port' => [1, 1],
'highest valid port' => [65535, 65535],
'highest valid port as string' => ['65535', 65535],

// unset or empty values fall back
'null port' => [null, 21],
'empty port' => ['', 21],
'whitespace port' => [' ', 21],
'array port' => [[2121], 21],

// non integer values fall back
'non numeric port' => ['ftp', 21],
'float port' => [21.5, 21],
'integer float port' => [2121.0, 21],
'decimal string port' => ['21.5', 21],
'exponential string port' => ['1e3', 21],
'hexadecimal string port' => ['0x15', 21],
'signed string port' => ['+2121', 21],
'padded string port' => [' 2121', 21],
'boolean port' => [true, 21],

// out of range values fall back
'zero port' => [0, 21],
'zero string port' => ['0', 21],
'negative port' => [-2121, 21],
'negative string port' => ['-2121', 21],
'too large port' => [65536, 21],
'too large string port' => ['65536', 21],
'way too large string port' => ['999999999999999999999999', 21],
];
}

#[DataProvider('portProvider')]
public function testParsePort(mixed $port, int $expectedPort): void {
$this->assertSame($expectedPort, PortHelper::parsePort($port, 21));
}

public function testParsePortReturnsGivenFallback(): void {
$this->assertSame(22, PortHelper::parsePort('', 22));
}
}
51 changes: 51 additions & 0 deletions apps/files_external/tests/SftpPortTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files_External\Tests;

use OCA\Files_External\Lib\Storage\SFTP;
use PHPUnit\Framework\Attributes\DataProvider;
use Test\TestCase;

class SftpPortTest extends TestCase {
public static function portProvider(): array {
$parameters = [
'host' => 'somehost',
'user' => 'someuser',
'password' => 'somepassword',
];

return [
'no port given' => [$parameters, 22],
'empty port' => [array_merge($parameters, ['port' => '']), 22],
'null port' => [array_merge($parameters, ['port' => null]), 22],
'non numeric port' => [array_merge($parameters, ['port' => 'sftp']), 22],
'numeric string port' => [array_merge($parameters, ['port' => '2222']), 2222],
'integer port' => [array_merge($parameters, ['port' => 2222]), 2222],
'decimal port' => [array_merge($parameters, ['port' => '22.5']), 22],
'zero port' => [array_merge($parameters, ['port' => '0']), 22],
'negative port' => [array_merge($parameters, ['port' => '-2222']), 22],
'out of range port' => [array_merge($parameters, ['port' => '65536']), 22],
'highest valid port' => [array_merge($parameters, ['port' => '65535']), 65535],

// the port can also be part of the host field
'port in host' => [array_merge($parameters, ['host' => 'somehost:2222']), 2222],
'port in host with empty port' => [array_merge($parameters, ['host' => 'somehost:2222', 'port' => '']), 2222],
'port in host overwritten by port' => [array_merge($parameters, ['host' => 'somehost:2222', 'port' => '2223']), 2223],
'port in host with invalid port' => [array_merge($parameters, ['host' => 'somehost:2222', 'port' => '65536']), 2222],
];
}

#[DataProvider('portProvider')]
public function testPort(array $parameters, int $expectedPort): void {
$instance = new SFTP($parameters);

$this->assertSame($expectedPort, self::invokePrivate($instance, 'port'));
}
}