@@ -0,0 +1,83 @@ | |||
/** | |||
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @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/>. | |||
* | |||
*/ | |||
(function() { | |||
OCA.WorkflowEngine = OCA.WorkflowEngine || {}; | |||
OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {}; | |||
OCA.WorkflowEngine.Plugins.RequestRemoteAddressPlugin = { | |||
getCheck: function() { | |||
return { | |||
'class': 'OCA\\WorkflowEngine\\Check\\RequestRemoteAddress', | |||
'name': t('workflowengine', 'Request remote address'), | |||
'operators': [ | |||
{'operator': 'matchesIPv4', 'name': t('workflowengine', 'matches IPv4')}, | |||
{'operator': '!matchesIPv4', 'name': t('workflowengine', 'does not match IPv4')}, | |||
{'operator': 'matchesIPv6', 'name': t('workflowengine', 'matches IPv6')}, | |||
{'operator': '!matchesIPv6', 'name': t('workflowengine', 'does not match IPv6')} | |||
] | |||
}; | |||
}, | |||
render: function(element, check) { | |||
if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\RequestRemoteAddress') { | |||
return; | |||
} | |||
var placeholder = '127.0.0.1/32'; // Do not translate!!! | |||
if (check['operator'] === 'matchesIPv6' || check['operator'] === '!matchesIPv6') { | |||
placeholder = '::1/128'; // Do not translate!!! | |||
if (this._validateIPv6(check['value'])) { | |||
$(element).removeClass('invalid-input'); | |||
} else { | |||
$(element).addClass('invalid-input'); | |||
} | |||
} else { | |||
if (this._validateIPv4(check['value'])) { | |||
$(element).removeClass('invalid-input'); | |||
} else { | |||
$(element).addClass('invalid-input'); | |||
} | |||
} | |||
$(element).css('width', '300px') | |||
.attr('placeholder', placeholder) | |||
.attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder})) | |||
.addClass('has-tooltip') | |||
.tooltip({ | |||
placement: 'bottom' | |||
}); | |||
}, | |||
_validateIPv4: function(string) { | |||
var regexRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(3[0-2]|[1-2][0-9]|[1-9])$/, | |||
result = regexRegex.exec(string); | |||
return result !== null; | |||
}, | |||
_validateIPv6: function(string) { | |||
var regexRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(1([01][0-9]|2[0-8])|[1-9][0-9]|[0-9])$/, | |||
result = regexRegex.exec(string); | |||
return result !== null; | |||
} | |||
}; | |||
})(); | |||
OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.RequestRemoteAddressPlugin); |
@@ -58,8 +58,9 @@ class Application extends \OCP\AppFramework\App { | |||
'filemimetypeplugin', | |||
'filesizeplugin', | |||
'filesystemtagsplugin', | |||
'requestuseragentplugin', | |||
'requestremoteaddressplugin', | |||
'requesturlplugin', | |||
'requestuseragentplugin', | |||
'usergroupmembershipplugin', | |||
]); | |||
}, |
@@ -0,0 +1,148 @@ | |||
<?php | |||
/** | |||
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @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 OCA\WorkflowEngine\Check; | |||
use OCP\Files\Storage\IStorage; | |||
use OCP\IRequest; | |||
use OCP\WorkflowEngine\ICheck; | |||
class RequestRemoteAddress implements ICheck { | |||
/** @var IRequest */ | |||
protected $request; | |||
/** | |||
* @param IRequest $request | |||
*/ | |||
public function __construct(IRequest $request) { | |||
$this->request = $request; | |||
} | |||
/** | |||
* @param IStorage $storage | |||
* @param string $path | |||
*/ | |||
public function setFileInfo(IStorage $storage, $path) { | |||
// A different path doesn't change time, so nothing to do here. | |||
} | |||
/** | |||
* @param string $operator | |||
* @param string $value | |||
* @return bool | |||
*/ | |||
public function executeCheck($operator, $value) { | |||
$actualValue = $this->request->getRemoteAddress(); | |||
$decodedValue = explode('/', $value); | |||
if ($operator === 'matchesIPv4') { | |||
return $this->matchIPv4($actualValue, $decodedValue[0], $decodedValue[1]); | |||
} else if ($operator === '!matchesIPv4') { | |||
return !$this->matchIPv4($actualValue, $decodedValue[0], $decodedValue[1]); | |||
} else if ($operator === 'matchesIPv6') { | |||
return $this->matchIPv6($actualValue, $decodedValue[0], $decodedValue[1]); | |||
} else { | |||
return !$this->matchIPv6($actualValue, $decodedValue[0], $decodedValue[1]); | |||
} | |||
} | |||
/** | |||
* @param string $operator | |||
* @param string $value | |||
* @throws \UnexpectedValueException | |||
*/ | |||
public function validateCheck($operator, $value) { | |||
if (!in_array($operator, ['matchesIPv4', '!matchesIPv4', 'matchesIPv6', '!matchesIPv6'])) { | |||
throw new \UnexpectedValueException('Invalid operator', 1); | |||
} | |||
$decodedValue = explode('/', $value); | |||
if (sizeof($decodedValue) !== 2) { | |||
throw new \UnexpectedValueException('Invalid IP range', 2); | |||
} | |||
if (in_array($operator, ['matchesIPv4', '!matchesIPv4'])) { | |||
if (!filter_var($decodedValue[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { | |||
throw new \UnexpectedValueException('Invalid IPv4 range', 3); | |||
} | |||
if ($decodedValue[1] > 32 || $decodedValue[1] <= 0) { | |||
throw new \UnexpectedValueException('Invalid IPv4 range', 4); | |||
} | |||
} else { | |||
if (!filter_var($decodedValue[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { | |||
throw new \UnexpectedValueException('Invalid IPv6 range', 3); | |||
} | |||
if ($decodedValue[1] > 128 || $decodedValue[1] <= 0) { | |||
throw new \UnexpectedValueException('Invalid IPv6 range', 4); | |||
} | |||
} | |||
} | |||
/** | |||
* Based on http://stackoverflow.com/a/594134 | |||
* @param string $ip | |||
* @param string $rangeIp | |||
* @param int $bits | |||
* @return bool | |||
*/ | |||
protected function matchIPv4($ip, $rangeIp, $bits) { | |||
$rangeDecimal = ip2long($rangeIp); | |||
$ipDecimal = ip2long($ip); | |||
$mask = -1 << (32 - $bits); | |||
return ($ipDecimal & $mask) === ($rangeDecimal & $mask); | |||
} | |||
/** | |||
* Based on http://stackoverflow.com/a/7951507 | |||
* @param string $ip | |||
* @param string $rangeIp | |||
* @param int $bits | |||
* @return bool | |||
*/ | |||
protected function matchIPv6($ip, $rangeIp, $bits) { | |||
$ipNet = inet_pton($ip); | |||
$binaryIp = $this->ipv6ToBits($ipNet); | |||
$ipNetBits = substr($binaryIp, 0, $bits); | |||
$rangeNet = inet_pton($rangeIp); | |||
$binaryRange = $this->ipv6ToBits($rangeNet); | |||
$rangeNetBits = substr($binaryRange, 0, $bits); | |||
return $ipNetBits === $rangeNetBits; | |||
} | |||
/** | |||
* Based on http://stackoverflow.com/a/7951507 | |||
* @param string $packedIp | |||
* @return string | |||
*/ | |||
protected function ipv6ToBits($packedIp) { | |||
$unpackedIp = unpack('A16', $packedIp); | |||
$unpackedIp = str_split($unpackedIp[1]); | |||
$binaryIp = ''; | |||
foreach ($unpackedIp as $char) { | |||
$binaryIp .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT); | |||
} | |||
return str_pad($binaryIp, 128, '0', STR_PAD_RIGHT); | |||
} | |||
} |
@@ -0,0 +1,123 @@ | |||
<?php | |||
/** | |||
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> | |||
* | |||
* @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 OCA\WorkflowEngine\Tests\Check; | |||
class RequestRemoteAddressTest extends \Test\TestCase { | |||
/** @var \OCP\IRequest|\PHPUnit_Framework_MockObject_MockObject */ | |||
protected $request; | |||
protected function setUp() { | |||
parent::setUp(); | |||
$this->request = $this->getMockBuilder('OCP\IRequest') | |||
->getMock(); | |||
} | |||
public function dataExecuteCheckIPv4() { | |||
return [ | |||
['127.0.0.1/32', '127.0.0.1', true], | |||
['127.0.0.1/32', '127.0.0.0', false], | |||
['127.0.0.1/31', '127.0.0.0', true], | |||
['127.0.0.1/32', '127.0.0.2', false], | |||
['127.0.0.1/31', '127.0.0.2', false], | |||
['127.0.0.1/30', '127.0.0.2', true], | |||
]; | |||
} | |||
/** | |||
* @dataProvider dataExecuteCheckIPv4 | |||
* @param string $value | |||
* @param string $ip | |||
* @param bool $expected | |||
*/ | |||
public function testExecuteCheckMatchesIPv4($value, $ip, $expected) { | |||
$check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->request); | |||
$this->request->expects($this->once()) | |||
->method('getRemoteAddress') | |||
->willReturn($ip); | |||
$this->assertEquals($expected, $check->executeCheck('matchesIPv4', $value)); | |||
} | |||
/** | |||
* @dataProvider dataExecuteCheckIPv4 | |||
* @param string $value | |||
* @param string $ip | |||
* @param bool $expected | |||
*/ | |||
public function testExecuteCheckNotMatchesIPv4($value, $ip, $expected) { | |||
$check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->request); | |||
$this->request->expects($this->once()) | |||
->method('getRemoteAddress') | |||
->willReturn($ip); | |||
$this->assertEquals(!$expected, $check->executeCheck('!matchesIPv4', $value)); | |||
} | |||
public function dataExecuteCheckIPv6() { | |||
return [ | |||
['::1/128', '::1', true], | |||
['::2/128', '::3', false], | |||
['::2/127', '::3', true], | |||
['::1/128', '::2', false], | |||
['::1/127', '::2', false], | |||
['::1/126', '::2', true], | |||
['1234::1/127', '1234::', true], | |||
]; | |||
} | |||
/** | |||
* @dataProvider dataExecuteCheckIPv6 | |||
* @param string $value | |||
* @param string $ip | |||
* @param bool $expected | |||
*/ | |||
public function testExecuteCheckMatchesIPv6($value, $ip, $expected) { | |||
$check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->request); | |||
$this->request->expects($this->once()) | |||
->method('getRemoteAddress') | |||
->willReturn($ip); | |||
$this->assertEquals($expected, $check->executeCheck('matchesIPv6', $value)); | |||
} | |||
/** | |||
* @dataProvider dataExecuteCheckIPv6 | |||
* @param string $value | |||
* @param string $ip | |||
* @param bool $expected | |||
*/ | |||
public function testExecuteCheckNotMatchesIPv6($value, $ip, $expected) { | |||
$check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->request); | |||
$this->request->expects($this->once()) | |||
->method('getRemoteAddress') | |||
->willReturn($ip); | |||
$this->assertEquals(!$expected, $check->executeCheck('!matchesIPv6', $value)); | |||
} | |||
} |