* Initial web executor * Fix PHPDoc Fix broken integration test OccControllerTests do not require database access - moch them all! Kill unused sprintftags/v9.1.0RC1
@@ -32,6 +32,7 @@ use OC\AppFramework\Utility\TimeFactory; | |||
use OC\Core\Controller\AvatarController; | |||
use OC\Core\Controller\LoginController; | |||
use OC\Core\Controller\LostController; | |||
use OC\Core\Controller\OccController; | |||
use OC\Core\Controller\TokenController; | |||
use OC\Core\Controller\TwoFactorChallengeController; | |||
use OC\Core\Controller\UserController; | |||
@@ -125,6 +126,18 @@ class Application extends App { | |||
$c->query('SecureRandom') | |||
); | |||
}); | |||
$container->registerService('OccController', function(SimpleContainer $c) { | |||
return new OccController( | |||
$c->query('AppName'), | |||
$c->query('Request'), | |||
$c->query('Config'), | |||
new \OC\Console\Application( | |||
$c->query('Config'), | |||
$c->query('ServerContainer')->getEventDispatcher(), | |||
$c->query('Request') | |||
) | |||
); | |||
}); | |||
/** | |||
* Core class wrappers |
@@ -0,0 +1,147 @@ | |||
<?php | |||
/** | |||
* @author Victor Dubiniuk <dubiniuk@owncloud.com> | |||
* | |||
* @copyright Copyright (c) 2016, ownCloud, Inc. | |||
* @license AGPL-3.0 | |||
* | |||
* This code is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License, version 3, | |||
* as published by the Free Software Foundation. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License, version 3, | |||
* along with this program. If not, see <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
namespace OC\Core\Controller; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http\JSONResponse; | |||
use OC\Console\Application; | |||
use OCP\IConfig; | |||
use OCP\IRequest; | |||
use Symfony\Component\Console\Input\ArrayInput; | |||
use Symfony\Component\Console\Output\BufferedOutput; | |||
class OccController extends Controller { | |||
/** @var array */ | |||
private $allowedCommands = [ | |||
'app:disable', | |||
'app:enable', | |||
'app:getpath', | |||
'app:list', | |||
'check', | |||
'config:list', | |||
'maintenance:mode', | |||
'status', | |||
'upgrade' | |||
]; | |||
/** @var IConfig */ | |||
private $config; | |||
/** @var Application */ | |||
private $console; | |||
/** | |||
* OccController constructor. | |||
* | |||
* @param string $appName | |||
* @param IRequest $request | |||
* @param IConfig $config | |||
* @param Application $console | |||
*/ | |||
public function __construct($appName, IRequest $request, | |||
IConfig $config, Application $console) { | |||
parent::__construct($appName, $request); | |||
$this->config = $config; | |||
$this->console = $console; | |||
} | |||
/** | |||
* @PublicPage | |||
* @NoCSRFRequired | |||
* | |||
* Execute occ command | |||
* Sample request | |||
* POST http://domain.tld/index.php/occ/status', | |||
* { | |||
* 'params': { | |||
* '--no-warnings':'1', | |||
* '--output':'json' | |||
* }, | |||
* 'token': 'someToken' | |||
* } | |||
* | |||
* @param string $command | |||
* @param string $token | |||
* @param array $params | |||
* | |||
* @return JSONResponse | |||
* @throws \Exception | |||
*/ | |||
public function execute($command, $token, $params = []) { | |||
try { | |||
$this->validateRequest($command, $token); | |||
$output = new BufferedOutput(); | |||
$formatter = $output->getFormatter(); | |||
$formatter->setDecorated(false); | |||
$this->console->setAutoExit(false); | |||
$this->console->loadCommands(new ArrayInput([]), $output); | |||
$inputArray = array_merge(['command' => $command], $params); | |||
$input = new ArrayInput($inputArray); | |||
$exitCode = $this->console->run($input, $output); | |||
$response = $output->fetch(); | |||
$json = [ | |||
'exitCode' => $exitCode, | |||
'response' => $response | |||
]; | |||
} catch (\UnexpectedValueException $e){ | |||
$json = [ | |||
'exitCode' => 126, | |||
'response' => 'Not allowed', | |||
'details' => $e->getMessage() | |||
]; | |||
} | |||
return new JSONResponse($json); | |||
} | |||
/** | |||
* Check if command is allowed and has a valid security token | |||
* @param $command | |||
* @param $token | |||
*/ | |||
protected function validateRequest($command, $token){ | |||
if (!in_array($this->request->getRemoteAddress(), ['::1', '127.0.0.1', 'localhost'])) { | |||
throw new \UnexpectedValueException('Web executor is not allowed to run from a different host'); | |||
} | |||
if (!in_array($command, $this->allowedCommands)) { | |||
throw new \UnexpectedValueException(sprintf('Command "%s" is not allowed to run via web request', $command)); | |||
} | |||
$coreToken = $this->config->getSystemValue('updater.secret', ''); | |||
if ($coreToken === '') { | |||
throw new \UnexpectedValueException( | |||
'updater.secret is undefined in config/config.php. Either browse the admin settings in your ownCloud and click "Open updater" or define a strong secret using <pre>php -r \'echo password_hash("MyStrongSecretDoUseYourOwn!", PASSWORD_DEFAULT)."\n";\'</pre> and set this in the config.php.' | |||
); | |||
} | |||
if (!password_verify($token, $coreToken)) { | |||
throw new \UnexpectedValueException( | |||
'updater.secret does not match the provided token' | |||
); | |||
} | |||
} | |||
} |
@@ -48,6 +48,7 @@ $application->registerRoutes($this, [ | |||
['name' => 'login#showLoginForm', 'url' => '/login', 'verb' => 'GET'], | |||
['name' => 'login#logout', 'url' => '/logout', 'verb' => 'GET'], | |||
['name' => 'token#generateToken', 'url' => '/token/generate', 'verb' => 'POST'], | |||
['name' => 'occ#execute', 'url' => '/occ/{command}', 'verb' => 'POST'], | |||
['name' => 'TwoFactorChallenge#selectChallenge', 'url' => '/login/selectchallenge', 'verb' => 'GET'], | |||
['name' => 'TwoFactorChallenge#showChallenge', 'url' => '/login/challenge/{challengeProviderId}', 'verb' => 'GET'], | |||
['name' => 'TwoFactorChallenge#solveChallenge', 'url' => '/login/challenge/{challengeProviderId}', 'verb' => 'POST'], |
@@ -49,6 +49,8 @@ | |||
* | |||
*/ | |||
use OCP\IRequest; | |||
require_once 'public/Constants.php'; | |||
/** | |||
@@ -271,9 +273,20 @@ class OC { | |||
} | |||
} | |||
public static function checkMaintenanceMode() { | |||
/** | |||
* Limit maintenance mode access | |||
* @param IRequest $request | |||
*/ | |||
public static function checkMaintenanceMode(IRequest $request) { | |||
// Check if requested URL matches 'index.php/occ' | |||
$isOccControllerRequested = preg_match('|/index\.php$|', $request->getScriptName()) === 1 | |||
&& strpos($request->getPathInfo(), '/occ/') === 0; | |||
// Allow ajax update script to execute without being stopped | |||
if (\OC::$server->getSystemConfig()->getValue('maintenance', false) && OC::$SUBURI != '/core/ajax/update.php') { | |||
if ( | |||
\OC::$server->getSystemConfig()->getValue('maintenance', false) | |||
&& OC::$SUBURI != '/core/ajax/update.php' | |||
&& !$isOccControllerRequested | |||
) { | |||
// send http status 503 | |||
header('HTTP/1.1 503 Service Temporarily Unavailable'); | |||
header('Status: 503 Service Temporarily Unavailable'); | |||
@@ -820,7 +833,7 @@ class OC { | |||
$request = \OC::$server->getRequest(); | |||
$requestPath = $request->getRawPathInfo(); | |||
if (substr($requestPath, -3) !== '.js') { // we need these files during the upgrade | |||
self::checkMaintenanceMode(); | |||
self::checkMaintenanceMode($request); | |||
self::checkUpgrade(); | |||
} | |||
@@ -138,9 +138,10 @@ class Application { | |||
* @throws \Exception | |||
*/ | |||
public function run(InputInterface $input = null, OutputInterface $output = null) { | |||
$args = isset($this->request->server['argv']) ? $this->request->server['argv'] : []; | |||
$this->dispatcher->dispatch(ConsoleEvent::EVENT_RUN, new ConsoleEvent( | |||
ConsoleEvent::EVENT_RUN, | |||
$this->request->server['argv'] | |||
$args | |||
)); | |||
return $this->application->run($input, $output); | |||
} |
@@ -35,9 +35,9 @@ try { | |||
exit; | |||
} | |||
OC::checkMaintenanceMode(); | |||
OC::checkSingleUserMode(true); | |||
$request = \OC::$server->getRequest(); | |||
OC::checkMaintenanceMode($request); | |||
OC::checkSingleUserMode(true); | |||
$pathInfo = $request->getPathInfo(); | |||
if (!$pathInfo && $request->getParam('service', '') === '') { |
@@ -0,0 +1,143 @@ | |||
<?php | |||
/** | |||
* @author Victor Dubiniuk <dubiniuk@owncloud.com> | |||
* | |||
* @copyright Copyright (c) 2015, ownCloud, Inc. | |||
* @license AGPL-3.0 | |||
* | |||
* This code is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License, version 3, | |||
* as published by the Free Software Foundation. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License, version 3, | |||
* along with this program. If not, see <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
namespace Tests\Core\Controller; | |||
use OC\Console\Application; | |||
use OC\Core\Controller\OccController; | |||
use OCP\IConfig; | |||
use Symfony\Component\Console\Output\Output; | |||
use Test\TestCase; | |||
/** | |||
* Class OccControllerTest | |||
* | |||
* @package OC\Core\Controller | |||
*/ | |||
class OccControllerTest extends TestCase { | |||
const TEMP_SECRET = 'test'; | |||
/** @var \OC\AppFramework\Http\Request | \PHPUnit_Framework_MockObject_MockObject */ | |||
private $request; | |||
/** @var \OC\Core\Controller\OccController | \PHPUnit_Framework_MockObject_MockObject */ | |||
private $controller; | |||
/** @var IConfig | \PHPUnit_Framework_MockObject_MockObject */ | |||
private $config; | |||
/** @var Application | \PHPUnit_Framework_MockObject_MockObject */ | |||
private $console; | |||
public function testFromInvalidLocation(){ | |||
$this->getControllerMock('example.org'); | |||
$response = $this->controller->execute('status', ''); | |||
$responseData = $response->getData(); | |||
$this->assertArrayHasKey('exitCode', $responseData); | |||
$this->assertEquals(126, $responseData['exitCode']); | |||
$this->assertArrayHasKey('details', $responseData); | |||
$this->assertEquals('Web executor is not allowed to run from a different host', $responseData['details']); | |||
} | |||
public function testNotWhiteListedCommand(){ | |||
$this->getControllerMock('localhost'); | |||
$response = $this->controller->execute('missing_command', ''); | |||
$responseData = $response->getData(); | |||
$this->assertArrayHasKey('exitCode', $responseData); | |||
$this->assertEquals(126, $responseData['exitCode']); | |||
$this->assertArrayHasKey('details', $responseData); | |||
$this->assertEquals('Command "missing_command" is not allowed to run via web request', $responseData['details']); | |||
} | |||
public function testWrongToken(){ | |||
$this->getControllerMock('localhost'); | |||
$response = $this->controller->execute('status', self::TEMP_SECRET . '-'); | |||
$responseData = $response->getData(); | |||
$this->assertArrayHasKey('exitCode', $responseData); | |||
$this->assertEquals(126, $responseData['exitCode']); | |||
$this->assertArrayHasKey('details', $responseData); | |||
$this->assertEquals('updater.secret does not match the provided token', $responseData['details']); | |||
} | |||
public function testSuccess(){ | |||
$this->getControllerMock('localhost'); | |||
$this->console->expects($this->once())->method('run') | |||
->willReturnCallback( | |||
function ($input, $output) { | |||
/** @var Output $output */ | |||
$output->writeln('{"installed":true,"version":"9.1.0.8","versionstring":"9.1.0 beta 2","edition":""}'); | |||
return 0; | |||
} | |||
); | |||
$response = $this->controller->execute('status', self::TEMP_SECRET, ['--output'=>'json']); | |||
$responseData = $response->getData(); | |||
$this->assertArrayHasKey('exitCode', $responseData); | |||
$this->assertEquals(0, $responseData['exitCode']); | |||
$this->assertArrayHasKey('response', $responseData); | |||
$decoded = json_decode($responseData['response'], true); | |||
$this->assertArrayHasKey('installed', $decoded); | |||
$this->assertEquals(true, $decoded['installed']); | |||
} | |||
private function getControllerMock($host){ | |||
$this->request = $this->getMockBuilder('OC\AppFramework\Http\Request') | |||
->setConstructorArgs([ | |||
['server' => []], | |||
\OC::$server->getSecureRandom(), | |||
\OC::$server->getConfig() | |||
]) | |||
->setMethods(['getRemoteAddress']) | |||
->getMock(); | |||
$this->request->expects($this->any())->method('getRemoteAddress') | |||
->will($this->returnValue($host)); | |||
$this->config = $this->getMockBuilder('\OCP\IConfig') | |||
->disableOriginalConstructor() | |||
->getMock(); | |||
$this->config->expects($this->any())->method('getSystemValue') | |||
->with('updater.secret') | |||
->willReturn(password_hash(self::TEMP_SECRET, PASSWORD_DEFAULT)); | |||
$this->console = $this->getMockBuilder('\OC\Console\Application') | |||
->disableOriginalConstructor() | |||
->getMock(); | |||
$this->controller = new OccController( | |||
'core', | |||
$this->request, | |||
$this->config, | |||
$this->console | |||
); | |||
} | |||
} |