Serialize NordVPN bootstrap and reconciliation
This commit is contained in:
parent
0e3c367bb7
commit
6b2049ccf7
4 changed files with 74 additions and 2 deletions
|
|
@ -10,6 +10,7 @@ use OPNsense\Core\Backend;
|
||||||
use OPNsense\NordVPN\EnrollmentInspector;
|
use OPNsense\NordVPN\EnrollmentInspector;
|
||||||
use OPNsense\NordVPN\ManagedConfiguration;
|
use OPNsense\NordVPN\ManagedConfiguration;
|
||||||
use OPNsense\NordVPN\NordApi;
|
use OPNsense\NordVPN\NordApi;
|
||||||
|
use OPNsense\NordVPN\ReconciliationLock;
|
||||||
use OPNsense\NordVPN\Reconciler;
|
use OPNsense\NordVPN\Reconciler;
|
||||||
use OPNsense\NordVPN\Settings;
|
use OPNsense\NordVPN\Settings;
|
||||||
use OPNsense\NordVPN\StatusStore;
|
use OPNsense\NordVPN\StatusStore;
|
||||||
|
|
@ -23,7 +24,9 @@ class ServiceController extends ApiControllerBase
|
||||||
return ['status' => 'failed', 'message' => 'POST is required.'];
|
return ['status' => 'failed', 'message' => 'POST is required.'];
|
||||||
}
|
}
|
||||||
$token = trim((string)($this->request->getPost('token') ?? ''));
|
$token = trim((string)($this->request->getPost('token') ?? ''));
|
||||||
|
$lock = new ReconciliationLock();
|
||||||
try {
|
try {
|
||||||
|
$lock->acquire();
|
||||||
$api = new NordApi();
|
$api = new NordApi();
|
||||||
$settings = new Settings();
|
$settings = new Settings();
|
||||||
$countries = array_values(array_filter(explode(',', (string)$settings->general->countries)));
|
$countries = array_values(array_filter(explode(',', (string)$settings->general->countries)));
|
||||||
|
|
@ -53,6 +56,8 @@ class ServiceController extends ApiControllerBase
|
||||||
} catch (\Throwable $exception) {
|
} catch (\Throwable $exception) {
|
||||||
unset($token);
|
unset($token);
|
||||||
return ['status' => 'failed', 'message' => 'Bootstrap failed. Check the token and plugin configuration.'];
|
return ['status' => 'failed', 'message' => 'Bootstrap failed. Check the token and plugin configuration.'];
|
||||||
|
} finally {
|
||||||
|
$lock->release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
// SPDX-FileCopyrightText: 2026 ms2k
|
||||||
|
|
||||||
|
namespace OPNsense\NordVPN;
|
||||||
|
|
||||||
|
class ReconciliationLock
|
||||||
|
{
|
||||||
|
private string $path;
|
||||||
|
private $handle;
|
||||||
|
|
||||||
|
public function __construct(string $path = '/var/run/nordvpn.lock')
|
||||||
|
{
|
||||||
|
$this->path = $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function acquire(): void
|
||||||
|
{
|
||||||
|
$handle = fopen($this->path, 'c');
|
||||||
|
if ($handle === false) {
|
||||||
|
throw new \RuntimeException('Unable to create the NordVPN operation lock.');
|
||||||
|
}
|
||||||
|
if (!flock($handle, LOCK_EX | LOCK_NB)) {
|
||||||
|
fclose($handle);
|
||||||
|
throw new \RuntimeException('A NordVPN operation is already running.');
|
||||||
|
}
|
||||||
|
$this->handle = $handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function release(): void
|
||||||
|
{
|
||||||
|
if (!is_resource($this->handle)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
flock($this->handle, LOCK_UN);
|
||||||
|
fclose($this->handle);
|
||||||
|
$this->handle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
$this->release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -77,13 +77,13 @@
|
||||||
$('#bootstrapAct').click(function() {
|
$('#bootstrapAct').click(function() {
|
||||||
const token = $('#accessToken').val();
|
const token = $('#accessToken').val();
|
||||||
$('#accessToken').val('');
|
$('#accessToken').val('');
|
||||||
$('#bootstrapAct').addClass('disabled');
|
$('#bootstrapAct').addClass('disabled').prop('disabled', true);
|
||||||
$('#bootstrap_progress').addClass('fa fa-spinner fa-pulse');
|
$('#bootstrap_progress').addClass('fa fa-spinner fa-pulse');
|
||||||
ajaxCall('/api/nordvpn/service/bootstrap', {token: token}, function(data) {
|
ajaxCall('/api/nordvpn/service/bootstrap', {token: token}, function(data) {
|
||||||
$('#bootstrapMsg').removeClass('hidden alert-success alert-danger');
|
$('#bootstrapMsg').removeClass('hidden alert-success alert-danger');
|
||||||
$('#bootstrapMsg').addClass(data.status === 'ok' ? 'alert-success' : 'alert-danger');
|
$('#bootstrapMsg').addClass(data.status === 'ok' ? 'alert-success' : 'alert-danger');
|
||||||
$('#bootstrapMsg').text(data.status === 'ok' ? '{{ lang._("Bootstrap completed") }}' : data.message);
|
$('#bootstrapMsg').text(data.status === 'ok' ? '{{ lang._("Bootstrap completed") }}' : data.message);
|
||||||
$('#bootstrapAct').removeClass('disabled');
|
$('#bootstrapAct').removeClass('disabled').prop('disabled', false);
|
||||||
$('#bootstrap_progress').removeClass('fa fa-spinner fa-pulse');
|
$('#bootstrap_progress').removeClass('fa fa-spinner fa-pulse');
|
||||||
refreshStatus();
|
refreshStatus();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
use OPNsense\NordVPN\EndpointStore;
|
use OPNsense\NordVPN\EndpointStore;
|
||||||
use OPNsense\NordVPN\EnrollmentInspector;
|
use OPNsense\NordVPN\EnrollmentInspector;
|
||||||
use OPNsense\NordVPN\NordApi;
|
use OPNsense\NordVPN\NordApi;
|
||||||
|
use OPNsense\NordVPN\ReconciliationLock;
|
||||||
use OPNsense\NordVPN\StatusStore;
|
use OPNsense\NordVPN\StatusStore;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
|
@ -58,4 +59,25 @@ final class StoreAndInspectorTest extends TestCase
|
||||||
|
|
||||||
unlink($path);
|
unlink($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testReconciliationLockRejectsConcurrentOperations(): void
|
||||||
|
{
|
||||||
|
$path = sys_get_temp_dir() . '/nordvpn-lock-' . bin2hex(random_bytes(8));
|
||||||
|
$first = new ReconciliationLock($path);
|
||||||
|
$second = new ReconciliationLock($path);
|
||||||
|
|
||||||
|
$first->acquire();
|
||||||
|
try {
|
||||||
|
$second->acquire();
|
||||||
|
$this->fail('A concurrent operation acquired the lock.');
|
||||||
|
} catch (RuntimeException $exception) {
|
||||||
|
$this->assertSame('A NordVPN operation is already running.', $exception->getMessage());
|
||||||
|
} finally {
|
||||||
|
$first->release();
|
||||||
|
}
|
||||||
|
|
||||||
|
$second->acquire();
|
||||||
|
$second->release();
|
||||||
|
unlink($path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue