Add nodes management and initial setup pages
- Implemented nodes management functionality in `nodes.php` including create, update, and delete actions. - Added form validation and error handling for node operations. - Created a new setup page in `setup.php` for initial administrator account creation. - Included user feedback messages for successful operations and errors. - Designed user interface for both nodes management and setup processes.
This commit is contained in:
446
pathvector-admin/lib/ASN.php
Normal file
446
pathvector-admin/lib/ASN.php
Normal file
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
/**
|
||||
* ASN Class
|
||||
*
|
||||
* Manages ASN (Autonomous System Number) configurations for multi-ASN deployments
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/FlatFileDB.php';
|
||||
require_once __DIR__ . '/Logger.php';
|
||||
require_once __DIR__ . '/Validator.php';
|
||||
|
||||
class ASN {
|
||||
private FlatFileDB $db;
|
||||
private Logger $logger;
|
||||
private Validator $validator;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $dataFile Path to ASNs JSON file
|
||||
* @param Logger $logger Logger instance
|
||||
*/
|
||||
public function __construct(string $dataFile, Logger $logger) {
|
||||
$this->db = new FlatFileDB($dataFile);
|
||||
$this->logger = $logger;
|
||||
$this->validator = new Validator();
|
||||
|
||||
// Initialize structure
|
||||
if (!$this->db->exists('asns')) {
|
||||
$this->db->set('asns', []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all ASNs
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAll(): array {
|
||||
return $this->db->get('asns') ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ASN by ID
|
||||
*
|
||||
* @param string $id ASN number as string key
|
||||
* @return array|null
|
||||
*/
|
||||
public function get(string $id): ?array {
|
||||
$asns = $this->db->get('asns') ?? [];
|
||||
return $asns[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new ASN
|
||||
*
|
||||
* @param array $data ASN data
|
||||
* @return array ['success' => bool, 'message' => string, 'errors' => array]
|
||||
*/
|
||||
public function create(array $data): array {
|
||||
$this->validator->clear();
|
||||
|
||||
// Validate ASN number
|
||||
if (!$this->validator->validateAsn($data['asn'] ?? null, 'asn')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $this->validator->getErrors(),
|
||||
];
|
||||
}
|
||||
|
||||
$asns = $this->db->get('asns') ?? [];
|
||||
$id = (string) $data['asn'];
|
||||
|
||||
// Check if ASN already exists
|
||||
if (isset($asns[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "ASN $id already exists",
|
||||
'errors' => ['asn' => 'ASN already exists'],
|
||||
];
|
||||
}
|
||||
|
||||
// Build ASN entry
|
||||
$asnData = [
|
||||
'asn' => (int) $data['asn'],
|
||||
'name' => $data['name'] ?? "AS{$data['asn']}",
|
||||
'description' => $data['description'] ?? '',
|
||||
'pathvector_defaults' => $this->buildPathvectorDefaults($data['pathvector_defaults'] ?? []),
|
||||
'templates' => $data['templates'] ?? [],
|
||||
'contacts' => $data['contacts'] ?? [],
|
||||
'metadata' => $data['metadata'] ?? [],
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
];
|
||||
|
||||
$asns[$id] = $asnData;
|
||||
|
||||
if ($this->db->set('asns', $asns)) {
|
||||
$this->logger->success('asn', "Created ASN: $id ({$asnData['name']})");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "ASN $id created successfully",
|
||||
'data' => $asnData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to save ASN',
|
||||
'errors' => ['database' => 'Failed to save ASN'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ASN
|
||||
*
|
||||
* @param string $id ASN ID
|
||||
* @param array $data Updated data
|
||||
* @return array
|
||||
*/
|
||||
public function update(string $id, array $data): array {
|
||||
$asns = $this->db->get('asns') ?? [];
|
||||
|
||||
if (!isset($asns[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "ASN $id not found",
|
||||
'errors' => ['asn' => 'ASN not found'],
|
||||
];
|
||||
}
|
||||
|
||||
// Merge with existing data
|
||||
$asnData = $asns[$id];
|
||||
|
||||
if (isset($data['name'])) {
|
||||
$asnData['name'] = $data['name'];
|
||||
}
|
||||
|
||||
if (isset($data['description'])) {
|
||||
$asnData['description'] = $data['description'];
|
||||
}
|
||||
|
||||
if (isset($data['pathvector_defaults'])) {
|
||||
$asnData['pathvector_defaults'] = $this->buildPathvectorDefaults(
|
||||
array_merge($asnData['pathvector_defaults'], $data['pathvector_defaults'])
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($data['templates'])) {
|
||||
$asnData['templates'] = $data['templates'];
|
||||
}
|
||||
|
||||
if (isset($data['contacts'])) {
|
||||
$asnData['contacts'] = $data['contacts'];
|
||||
}
|
||||
|
||||
if (isset($data['metadata'])) {
|
||||
$asnData['metadata'] = array_merge($asnData['metadata'] ?? [], $data['metadata']);
|
||||
}
|
||||
|
||||
$asnData['updated_at'] = date('c');
|
||||
$asns[$id] = $asnData;
|
||||
|
||||
if ($this->db->set('asns', $asns)) {
|
||||
$this->logger->info('asn', "Updated ASN: $id");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "ASN $id updated successfully",
|
||||
'data' => $asnData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to update ASN',
|
||||
'errors' => ['database' => 'Failed to save ASN'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete ASN
|
||||
*
|
||||
* @param string $id ASN ID
|
||||
* @return array
|
||||
*/
|
||||
public function delete(string $id): array {
|
||||
$asns = $this->db->get('asns') ?? [];
|
||||
|
||||
if (!isset($asns[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "ASN $id not found",
|
||||
];
|
||||
}
|
||||
|
||||
$asnName = $asns[$id]['name'];
|
||||
unset($asns[$id]);
|
||||
|
||||
if ($this->db->set('asns', $asns)) {
|
||||
$this->logger->warning('asn', "Deleted ASN: $id ($asnName)");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "ASN $id deleted successfully",
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to delete ASN',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Pathvector defaults structure
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
private function buildPathvectorDefaults(array $data): array {
|
||||
return [
|
||||
// Global config
|
||||
'router_id' => $data['router_id'] ?? '',
|
||||
'source4' => $data['source4'] ?? '',
|
||||
'source6' => $data['source6'] ?? '',
|
||||
'prefixes' => $data['prefixes'] ?? [],
|
||||
'hostname' => $data['hostname'] ?? '',
|
||||
|
||||
// BIRD/Pathvector paths
|
||||
'bird_directory' => $data['bird_directory'] ?? '/etc/bird',
|
||||
'bird_binary' => $data['bird_binary'] ?? '/usr/sbin/bird',
|
||||
'bird_socket' => $data['bird_socket'] ?? '/var/run/bird.ctl',
|
||||
'cache_directory' => $data['cache_directory'] ?? '/var/cache/pathvector',
|
||||
|
||||
// PeeringDB settings
|
||||
'peeringdb_query_timeout' => $data['peeringdb_query_timeout'] ?? 10,
|
||||
'peeringdb_api_key' => $data['peeringdb_api_key'] ?? '',
|
||||
'peeringdb_cache' => $data['peeringdb_cache'] ?? true,
|
||||
'peeringdb_url' => $data['peeringdb_url'] ?? 'https://peeringdb.com/api/',
|
||||
|
||||
// IRR settings
|
||||
'irr_server' => $data['irr_server'] ?? 'rr.ntt.net',
|
||||
'irr_query_timeout' => $data['irr_query_timeout'] ?? 30,
|
||||
'bgpq_args' => $data['bgpq_args'] ?? '',
|
||||
|
||||
// RTR/RPKI settings
|
||||
'rtr_server' => $data['rtr_server'] ?? '',
|
||||
'rpki_enable' => $data['rpki_enable'] ?? true,
|
||||
|
||||
// Filtering defaults
|
||||
'keep_filtered' => $data['keep_filtered'] ?? false,
|
||||
'merge_paths' => $data['merge_paths'] ?? false,
|
||||
'default_route' => $data['default_route'] ?? false,
|
||||
'accept_default' => $data['accept_default'] ?? false,
|
||||
|
||||
// Communities
|
||||
'origin_communities' => $data['origin_communities'] ?? [],
|
||||
'local_communities' => $data['local_communities'] ?? [],
|
||||
'add_on_import' => $data['add_on_import'] ?? [],
|
||||
'add_on_export' => $data['add_on_export'] ?? [],
|
||||
|
||||
// Blocklist
|
||||
'blocklist' => $data['blocklist'] ?? [],
|
||||
'blocklist_urls' => $data['blocklist_urls'] ?? [],
|
||||
'blocklist_files' => $data['blocklist_files'] ?? [],
|
||||
|
||||
// Bogons
|
||||
'bogons4' => $data['bogons4'] ?? [],
|
||||
'bogons6' => $data['bogons6'] ?? [],
|
||||
'bogon_asns' => $data['bogon_asns'] ?? [],
|
||||
'blackhole_bogon_asns' => $data['blackhole_bogon_asns'] ?? false,
|
||||
|
||||
// Transit ASNs
|
||||
'transit_asns' => $data['transit_asns'] ?? [],
|
||||
|
||||
// Operation modes
|
||||
'no_announce' => $data['no_announce'] ?? false,
|
||||
'no_accept' => $data['no_accept'] ?? false,
|
||||
'stun' => $data['stun'] ?? false,
|
||||
|
||||
// Global config
|
||||
'global_config' => $data['global_config'] ?? '',
|
||||
|
||||
// Web UI
|
||||
'web_ui_file' => $data['web_ui_file'] ?? '',
|
||||
|
||||
// Log
|
||||
'log_file' => $data['log_file'] ?? 'syslog',
|
||||
|
||||
// Keepalived
|
||||
'keepalived_config' => $data['keepalived_config'] ?? '/etc/keepalived.conf',
|
||||
|
||||
// Authorized providers (ASPA)
|
||||
'authorized_providers' => $data['authorized_providers'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Count ASNs
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int {
|
||||
$asns = $this->db->get('asns') ?? [];
|
||||
return count($asns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search ASNs
|
||||
*
|
||||
* @param string $query
|
||||
* @return array
|
||||
*/
|
||||
public function search(string $query): array {
|
||||
$asns = $this->db->get('asns') ?? [];
|
||||
$query = strtolower($query);
|
||||
|
||||
return array_filter($asns, function($asn) use ($query) {
|
||||
return strpos(strtolower((string) $asn['asn']), $query) !== false ||
|
||||
strpos(strtolower($asn['name']), $query) !== false ||
|
||||
strpos(strtolower($asn['description'] ?? ''), $query) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Export ASN to YAML format for Pathvector
|
||||
*
|
||||
* @param string $id
|
||||
* @return string|null
|
||||
*/
|
||||
public function exportYaml(string $id): ?string {
|
||||
$asn = $this->get($id);
|
||||
|
||||
if (!$asn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$config = [];
|
||||
$config['asn'] = $asn['asn'];
|
||||
|
||||
$defaults = $asn['pathvector_defaults'];
|
||||
|
||||
if (!empty($defaults['router_id'])) {
|
||||
$config['router-id'] = $defaults['router_id'];
|
||||
}
|
||||
|
||||
if (!empty($defaults['source4'])) {
|
||||
$config['source4'] = $defaults['source4'];
|
||||
}
|
||||
|
||||
if (!empty($defaults['source6'])) {
|
||||
$config['source6'] = $defaults['source6'];
|
||||
}
|
||||
|
||||
if (!empty($defaults['prefixes'])) {
|
||||
$config['prefixes'] = $defaults['prefixes'];
|
||||
}
|
||||
|
||||
if (!empty($defaults['hostname'])) {
|
||||
$config['hostname'] = $defaults['hostname'];
|
||||
}
|
||||
|
||||
// Add other non-empty defaults
|
||||
foreach ($defaults as $key => $value) {
|
||||
if ($value !== '' && $value !== [] && $value !== null && !isset($config[$key])) {
|
||||
$yamlKey = str_replace('_', '-', $key);
|
||||
$config[$yamlKey] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->arrayToYaml($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert array to YAML string
|
||||
*
|
||||
* @param array $data
|
||||
* @param int $indent
|
||||
* @return string
|
||||
*/
|
||||
private function arrayToYaml(array $data, int $indent = 0): string {
|
||||
$yaml = '';
|
||||
$prefix = str_repeat(' ', $indent);
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
if (empty($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's a sequential array
|
||||
if (array_keys($value) === range(0, count($value) - 1)) {
|
||||
$yaml .= "$prefix$key:\n";
|
||||
foreach ($value as $item) {
|
||||
if (is_array($item)) {
|
||||
$yaml .= "$prefix -\n" . $this->arrayToYaml($item, $indent + 2);
|
||||
} else {
|
||||
$yaml .= "$prefix - " . $this->formatYamlValue($item) . "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$yaml .= "$prefix$key:\n" . $this->arrayToYaml($value, $indent + 1);
|
||||
}
|
||||
} else {
|
||||
if ($value === '' || $value === null) {
|
||||
continue;
|
||||
}
|
||||
$yaml .= "$prefix$key: " . $this->formatYamlValue($value) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $yaml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format value for YAML output
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
private function formatYamlValue($value): string {
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
if (is_string($value) && (strpos($value, ':') !== false || strpos($value, '#') !== false)) {
|
||||
return '"' . addslashes($value) . '"';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup ASN data
|
||||
*
|
||||
* @param string $backupDir
|
||||
* @return string|false
|
||||
*/
|
||||
public function backup(string $backupDir): string|false {
|
||||
return $this->db->backup($backupDir);
|
||||
}
|
||||
}
|
||||
428
pathvector-admin/lib/Auth.php
Normal file
428
pathvector-admin/lib/Auth.php
Normal file
@@ -0,0 +1,428 @@
|
||||
<?php
|
||||
/**
|
||||
* Auth Class
|
||||
*
|
||||
* Handles authentication and authorization for the Pathvector admin dashboard
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/FlatFileDB.php';
|
||||
require_once __DIR__ . '/Logger.php';
|
||||
|
||||
class Auth {
|
||||
private FlatFileDB $db;
|
||||
private Logger $logger;
|
||||
private array $config;
|
||||
private array $roles = [
|
||||
'admin' => ['admin', 'operator', 'readonly'],
|
||||
'operator' => ['operator', 'readonly'],
|
||||
'readonly' => ['readonly'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $userFile Path to users JSON file
|
||||
* @param Logger $logger Logger instance
|
||||
* @param array $config Application config
|
||||
*/
|
||||
public function __construct(string $userFile, Logger $logger, array $config) {
|
||||
$this->db = new FlatFileDB($userFile);
|
||||
$this->logger = $logger;
|
||||
$this->config = $config;
|
||||
|
||||
// Initialize users if empty
|
||||
if (!$this->db->exists('users')) {
|
||||
$this->initializeDefaultAdmin();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default admin user
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function initializeDefaultAdmin(): void {
|
||||
$defaultAdmin = $this->config['default_admin'] ?? [
|
||||
'username' => 'admin',
|
||||
'password' => 'pathvector',
|
||||
'role' => 'admin',
|
||||
];
|
||||
|
||||
$users = [
|
||||
$defaultAdmin['username'] => [
|
||||
'username' => $defaultAdmin['username'],
|
||||
'password' => password_hash($defaultAdmin['password'], PASSWORD_DEFAULT),
|
||||
'role' => $defaultAdmin['role'],
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
'last_login' => null,
|
||||
'is_active' => true,
|
||||
],
|
||||
];
|
||||
|
||||
$this->db->set('users', $users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start session if not already started
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function startSession(): void {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSRF token
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateCsrfToken(): string {
|
||||
$this->startSession();
|
||||
|
||||
if (!isset($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate CSRF token
|
||||
*
|
||||
* @param string $token
|
||||
* @return bool
|
||||
*/
|
||||
public function validateCsrfToken(string $token): bool {
|
||||
$this->startSession();
|
||||
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate CSRF token
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function regenerateCsrfToken(): string {
|
||||
$this->startSession();
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate user
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return bool
|
||||
*/
|
||||
public function login(string $username, string $password): bool {
|
||||
$this->startSession();
|
||||
|
||||
$users = $this->db->get('users') ?? [];
|
||||
|
||||
if (!isset($users[$username])) {
|
||||
$this->logger->warning('auth', "Failed login attempt for unknown user: $username");
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = $users[$username];
|
||||
|
||||
if (!$user['is_active']) {
|
||||
$this->logger->warning('auth', "Login attempt for inactive user: $username");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!password_verify($password, $user['password'])) {
|
||||
$this->logger->warning('auth', "Failed login attempt for user: $username");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update last login
|
||||
$users[$username]['last_login'] = date('c');
|
||||
$this->db->set('users', $users);
|
||||
|
||||
// Set session
|
||||
$_SESSION['user'] = [
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'],
|
||||
'logged_in_at' => date('c'),
|
||||
];
|
||||
|
||||
// Regenerate session ID for security
|
||||
session_regenerate_id(true);
|
||||
|
||||
$this->logger->success('auth', "User logged in: $username");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function logout(): void {
|
||||
$this->startSession();
|
||||
|
||||
$username = $_SESSION['user']['username'] ?? 'unknown';
|
||||
$this->logger->info('auth', "User logged out: $username");
|
||||
|
||||
$_SESSION = [];
|
||||
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(
|
||||
session_name(),
|
||||
'',
|
||||
time() - 42000,
|
||||
$params['path'],
|
||||
$params['domain'],
|
||||
$params['secure'],
|
||||
$params['httponly']
|
||||
);
|
||||
}
|
||||
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is logged in
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLoggedIn(): bool {
|
||||
$this->startSession();
|
||||
return isset($_SESSION['user']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getCurrentUser(): ?array {
|
||||
$this->startSession();
|
||||
return $_SESSION['user'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user has role
|
||||
*
|
||||
* @param string $requiredRole
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRole(string $requiredRole): bool {
|
||||
$this->startSession();
|
||||
|
||||
if (!isset($_SESSION['user']['role'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userRole = $_SESSION['user']['role'];
|
||||
|
||||
return in_array($requiredRole, $this->roles[$userRole] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Require login - redirect if not logged in
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function requireLogin(): void {
|
||||
if (!$this->isLoggedIn()) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require specific role
|
||||
*
|
||||
* @param string $role
|
||||
* @return void
|
||||
*/
|
||||
public function requireRole(string $role): void {
|
||||
$this->requireLogin();
|
||||
|
||||
if (!$this->hasRole($role)) {
|
||||
header('HTTP/1.1 403 Forbidden');
|
||||
echo 'Access Denied: Insufficient permissions';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new user
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string $role
|
||||
* @return bool
|
||||
*/
|
||||
public function createUser(string $username, string $password, string $role = 'readonly'): bool {
|
||||
$users = $this->db->get('users') ?? [];
|
||||
|
||||
if (isset($users[$username])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$users[$username] = [
|
||||
'username' => $username,
|
||||
'password' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'role' => $role,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
'last_login' => null,
|
||||
'is_active' => true,
|
||||
];
|
||||
|
||||
$result = $this->db->set('users', $users);
|
||||
|
||||
if ($result) {
|
||||
$this->logger->success('auth', "User created: $username with role: $role");
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user
|
||||
*
|
||||
* @param string $username
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function updateUser(string $username, array $data): bool {
|
||||
$users = $this->db->get('users') ?? [];
|
||||
|
||||
if (!isset($users[$username])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($data['password'])) {
|
||||
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
$data['updated_at'] = date('c');
|
||||
$users[$username] = array_merge($users[$username], $data);
|
||||
|
||||
$result = $this->db->set('users', $users);
|
||||
|
||||
if ($result) {
|
||||
$this->logger->info('auth', "User updated: $username");
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user
|
||||
*
|
||||
* @param string $username
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteUser(string $username): bool {
|
||||
$users = $this->db->get('users') ?? [];
|
||||
|
||||
if (!isset($users[$username])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset($users[$username]);
|
||||
|
||||
$result = $this->db->set('users', $users);
|
||||
|
||||
if ($result) {
|
||||
$this->logger->info('auth', "User deleted: $username");
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by username
|
||||
*
|
||||
* @param string $username
|
||||
* @return array|null
|
||||
*/
|
||||
public function getUser(string $username): ?array {
|
||||
$users = $this->db->get('users') ?? [];
|
||||
return $users[$username] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAllUsers(): array {
|
||||
$users = $this->db->get('users') ?? [];
|
||||
|
||||
// Remove password hashes for security
|
||||
return array_map(function($user) {
|
||||
unset($user['password']);
|
||||
return $user;
|
||||
}, $users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change user password
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $currentPassword
|
||||
* @param string $newPassword
|
||||
* @return bool
|
||||
*/
|
||||
public function changePassword(string $username, string $currentPassword, string $newPassword): bool {
|
||||
$users = $this->db->get('users') ?? [];
|
||||
|
||||
if (!isset($users[$username])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!password_verify($currentPassword, $users[$username]['password'])) {
|
||||
$this->logger->warning('auth', "Password change failed for user: $username (wrong current password)");
|
||||
return false;
|
||||
}
|
||||
|
||||
$users[$username]['password'] = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||
$users[$username]['updated_at'] = date('c');
|
||||
|
||||
$result = $this->db->set('users', $users);
|
||||
|
||||
if ($result) {
|
||||
$this->logger->success('auth', "Password changed for user: $username");
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user password (admin function)
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $newPassword
|
||||
* @return bool
|
||||
*/
|
||||
public function resetPassword(string $username, string $newPassword): bool {
|
||||
$users = $this->db->get('users') ?? [];
|
||||
|
||||
if (!isset($users[$username])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$users[$username]['password'] = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||
$users[$username]['updated_at'] = date('c');
|
||||
|
||||
$result = $this->db->set('users', $users);
|
||||
|
||||
if ($result) {
|
||||
$this->logger->success('auth', "Password reset for user: $username by admin");
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
457
pathvector-admin/lib/BirdConfig.php
Normal file
457
pathvector-admin/lib/BirdConfig.php
Normal file
@@ -0,0 +1,457 @@
|
||||
<?php
|
||||
/**
|
||||
* BirdConfig Class
|
||||
*
|
||||
* Helper class for BIRD 3 configuration parsing and generation
|
||||
*/
|
||||
|
||||
class BirdConfig {
|
||||
|
||||
/**
|
||||
* Parse BIRD protocol status output
|
||||
*
|
||||
* @param string $output
|
||||
* @return array
|
||||
*/
|
||||
public static function parseProtocolStatus(string $output): array {
|
||||
$protocols = [];
|
||||
$lines = explode("\n", trim($output));
|
||||
|
||||
// Skip header lines
|
||||
$dataStarted = false;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip BIRD header
|
||||
if (strpos($line, 'BIRD') === 0 || strpos($line, 'Name') === 0) {
|
||||
$dataStarted = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$dataStarted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse protocol line
|
||||
// Format: Name Proto Table State Since Info
|
||||
$parts = preg_split('/\s+/', $line, 6);
|
||||
|
||||
if (count($parts) >= 5) {
|
||||
$protocols[] = [
|
||||
'name' => $parts[0],
|
||||
'proto' => $parts[1],
|
||||
'table' => $parts[2],
|
||||
'state' => $parts[3],
|
||||
'since' => $parts[4],
|
||||
'info' => $parts[5] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $protocols;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse BIRD route count output
|
||||
*
|
||||
* @param string $output
|
||||
* @return array
|
||||
*/
|
||||
public static function parseRouteCount(string $output): array {
|
||||
$counts = [
|
||||
'total' => 0,
|
||||
'primary' => 0,
|
||||
'filtered' => 0,
|
||||
];
|
||||
|
||||
// Format: X of Y routes for Z networks
|
||||
if (preg_match('/(\d+)\s+of\s+(\d+)\s+routes\s+for\s+(\d+)\s+networks/', $output, $matches)) {
|
||||
$counts['primary'] = (int) $matches[1];
|
||||
$counts['total'] = (int) $matches[2];
|
||||
$counts['networks'] = (int) $matches[3];
|
||||
}
|
||||
|
||||
// Filtered routes
|
||||
if (preg_match('/(\d+)\s+routes\s+\((\d+)\s+filtered\)/', $output, $matches)) {
|
||||
$counts['total'] = (int) $matches[1];
|
||||
$counts['filtered'] = (int) $matches[2];
|
||||
}
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse BIRD status output
|
||||
*
|
||||
* @param string $output
|
||||
* @return array
|
||||
*/
|
||||
public static function parseStatus(string $output): array {
|
||||
$status = [
|
||||
'version' => '',
|
||||
'router_id' => '',
|
||||
'hostname' => '',
|
||||
'server_time' => '',
|
||||
'last_reboot' => '',
|
||||
'last_reconfiguration' => '',
|
||||
];
|
||||
|
||||
$lines = explode("\n", trim($output));
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
if (preg_match('/^BIRD\s+(.+)$/', $line, $matches)) {
|
||||
$status['version'] = trim($matches[1]);
|
||||
} elseif (preg_match('/^Router ID is\s+(.+)$/', $line, $matches)) {
|
||||
$status['router_id'] = trim($matches[1]);
|
||||
} elseif (preg_match('/^Hostname is\s+(.+)$/', $line, $matches)) {
|
||||
$status['hostname'] = trim($matches[1]);
|
||||
} elseif (preg_match('/^Current server time is\s+(.+)$/', $line, $matches)) {
|
||||
$status['server_time'] = trim($matches[1]);
|
||||
} elseif (preg_match('/^Last reboot on\s+(.+)$/', $line, $matches)) {
|
||||
$status['last_reboot'] = trim($matches[1]);
|
||||
} elseif (preg_match('/^Last reconfiguration on\s+(.+)$/', $line, $matches)) {
|
||||
$status['last_reconfiguration'] = trim($matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse detailed protocol info
|
||||
*
|
||||
* @param string $output
|
||||
* @return array
|
||||
*/
|
||||
public static function parseProtocolDetail(string $output): array {
|
||||
$info = [
|
||||
'name' => '',
|
||||
'description' => '',
|
||||
'state' => '',
|
||||
'table' => '',
|
||||
'neighbor_address' => '',
|
||||
'neighbor_as' => '',
|
||||
'local_as' => '',
|
||||
'neighbor_id' => '',
|
||||
'routes' => [
|
||||
'imported' => 0,
|
||||
'exported' => 0,
|
||||
'preferred' => 0,
|
||||
'filtered' => 0,
|
||||
],
|
||||
'uptime' => '',
|
||||
'hold_timer' => '',
|
||||
'keepalive_timer' => '',
|
||||
'last_error' => '',
|
||||
'channel' => [],
|
||||
];
|
||||
|
||||
$lines = explode("\n", trim($output));
|
||||
$currentChannel = null;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// Protocol name
|
||||
if (preg_match('/^(\S+)\s+BGP\s+/', $line, $matches)) {
|
||||
$info['name'] = $matches[1];
|
||||
}
|
||||
|
||||
// Description
|
||||
if (preg_match('/Description:\s+(.+)$/', $line, $matches)) {
|
||||
$info['description'] = trim($matches[1]);
|
||||
}
|
||||
|
||||
// State
|
||||
if (preg_match('/BGP state:\s+(\S+)/', $line, $matches)) {
|
||||
$info['state'] = $matches[1];
|
||||
}
|
||||
|
||||
// Neighbor address and AS
|
||||
if (preg_match('/Neighbor address:\s+(\S+)/', $line, $matches)) {
|
||||
$info['neighbor_address'] = $matches[1];
|
||||
}
|
||||
|
||||
if (preg_match('/Neighbor AS:\s+(\d+)/', $line, $matches)) {
|
||||
$info['neighbor_as'] = (int) $matches[1];
|
||||
}
|
||||
|
||||
if (preg_match('/Local AS:\s+(\d+)/', $line, $matches)) {
|
||||
$info['local_as'] = (int) $matches[1];
|
||||
}
|
||||
|
||||
if (preg_match('/Neighbor ID:\s+(\S+)/', $line, $matches)) {
|
||||
$info['neighbor_id'] = $matches[1];
|
||||
}
|
||||
|
||||
// Routes
|
||||
if (preg_match('/Routes:\s+(\d+)\s+imported,\s+(\d+)\s+exported,\s+(\d+)\s+preferred/', $line, $matches)) {
|
||||
$info['routes']['imported'] = (int) $matches[1];
|
||||
$info['routes']['exported'] = (int) $matches[2];
|
||||
$info['routes']['preferred'] = (int) $matches[3];
|
||||
}
|
||||
|
||||
if (preg_match('/(\d+)\s+filtered/', $line, $matches)) {
|
||||
$info['routes']['filtered'] = (int) $matches[1];
|
||||
}
|
||||
|
||||
// Hold timer
|
||||
if (preg_match('/Hold timer:\s+(\S+)/', $line, $matches)) {
|
||||
$info['hold_timer'] = $matches[1];
|
||||
}
|
||||
|
||||
// Keepalive timer
|
||||
if (preg_match('/Keepalive timer:\s+(\S+)/', $line, $matches)) {
|
||||
$info['keepalive_timer'] = $matches[1];
|
||||
}
|
||||
|
||||
// Last error
|
||||
if (preg_match('/Last error:\s+(.+)$/', $line, $matches)) {
|
||||
$info['last_error'] = trim($matches[1]);
|
||||
}
|
||||
|
||||
// Channel info
|
||||
if (preg_match('/Channel (ipv[46])/', $line, $matches)) {
|
||||
$currentChannel = $matches[1];
|
||||
$info['channel'][$currentChannel] = [];
|
||||
}
|
||||
|
||||
if ($currentChannel && preg_match('/State:\s+(\S+)/', $line, $matches)) {
|
||||
$info['channel'][$currentChannel]['state'] = $matches[1];
|
||||
}
|
||||
|
||||
if ($currentChannel && preg_match('/Table:\s+(\S+)/', $line, $matches)) {
|
||||
$info['channel'][$currentChannel]['table'] = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get state color class for UI
|
||||
*
|
||||
* @param string $state
|
||||
* @return string
|
||||
*/
|
||||
public static function getStateColorClass(string $state): string {
|
||||
return match (strtolower($state)) {
|
||||
'established' => 'color-fg-success',
|
||||
'up' => 'color-fg-success',
|
||||
'start', 'connect', 'active', 'opensent', 'openconfirm' => 'color-fg-attention',
|
||||
'idle' => 'color-fg-muted',
|
||||
'down' => 'color-fg-danger',
|
||||
default => 'color-fg-default',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get state label for UI
|
||||
*
|
||||
* @param string $state
|
||||
* @return string
|
||||
*/
|
||||
public static function getStateLabel(string $state): string {
|
||||
return match (strtolower($state)) {
|
||||
'established' => 'Label--success',
|
||||
'up' => 'Label--success',
|
||||
'start', 'connect', 'active', 'opensent', 'openconfirm' => 'Label--attention',
|
||||
'idle' => 'Label--secondary',
|
||||
'down' => 'Label--danger',
|
||||
default => 'Label--primary',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate basic BIRD 3 configuration
|
||||
*
|
||||
* @param array $config
|
||||
* @return string
|
||||
*/
|
||||
public static function generateBasicConfig(array $config): string {
|
||||
$bird = "# BIRD 3 Configuration\n";
|
||||
$bird .= "# Generated by Pathvector Admin\n\n";
|
||||
|
||||
// Router ID
|
||||
if (!empty($config['router_id'])) {
|
||||
$bird .= "router id {$config['router_id']};\n\n";
|
||||
}
|
||||
|
||||
// Log
|
||||
$bird .= "log syslog all;\n\n";
|
||||
|
||||
// Debug
|
||||
$bird .= "debug protocols all;\n\n";
|
||||
|
||||
// Timeformat
|
||||
$bird .= "timeformat protocol iso long;\n\n";
|
||||
|
||||
// Protocol device
|
||||
$bird .= "protocol device {\n";
|
||||
$bird .= " scan time 10;\n";
|
||||
$bird .= "}\n\n";
|
||||
|
||||
// Protocol direct
|
||||
$bird .= "protocol direct {\n";
|
||||
$bird .= " ipv4;\n";
|
||||
$bird .= " ipv6;\n";
|
||||
$bird .= "}\n\n";
|
||||
|
||||
// Protocol kernel
|
||||
$bird .= "protocol kernel {\n";
|
||||
$bird .= " ipv4 {\n";
|
||||
$bird .= " export all;\n";
|
||||
$bird .= " import all;\n";
|
||||
$bird .= " };\n";
|
||||
$bird .= "}\n\n";
|
||||
|
||||
$bird .= "protocol kernel {\n";
|
||||
$bird .= " ipv6 {\n";
|
||||
$bird .= " export all;\n";
|
||||
$bird .= " import all;\n";
|
||||
$bird .= " };\n";
|
||||
$bird .= "}\n\n";
|
||||
|
||||
return $bird;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate BIRD config syntax (basic)
|
||||
*
|
||||
* @param string $config
|
||||
* @return array
|
||||
*/
|
||||
public static function validateSyntax(string $config): array {
|
||||
$errors = [];
|
||||
$warnings = [];
|
||||
|
||||
// Check for common issues
|
||||
$lines = explode("\n", $config);
|
||||
$braceCount = 0;
|
||||
$inString = false;
|
||||
|
||||
foreach ($lines as $lineNum => $line) {
|
||||
$lineNum++; // 1-indexed
|
||||
|
||||
// Skip comments
|
||||
if (preg_match('/^\s*#/', $line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Count braces
|
||||
for ($i = 0; $i < strlen($line); $i++) {
|
||||
$char = $line[$i];
|
||||
|
||||
if ($char === '"' && ($i === 0 || $line[$i - 1] !== '\\')) {
|
||||
$inString = !$inString;
|
||||
}
|
||||
|
||||
if (!$inString) {
|
||||
if ($char === '{') {
|
||||
$braceCount++;
|
||||
} elseif ($char === '}') {
|
||||
$braceCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for missing semicolons (basic check)
|
||||
$trimmed = trim($line);
|
||||
if (!empty($trimmed) &&
|
||||
!preg_match('/[{};#]$/', $trimmed) &&
|
||||
!preg_match('/^\s*(protocol|filter|function|table|if|else|for|case|switch)/', $trimmed)) {
|
||||
$warnings[] = "Line $lineNum: Possible missing semicolon";
|
||||
}
|
||||
}
|
||||
|
||||
if ($braceCount !== 0) {
|
||||
$errors[] = "Unbalanced braces: " . ($braceCount > 0 ? "missing $braceCount closing braces" : "extra " . abs($braceCount) . " closing braces");
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors,
|
||||
'warnings' => $warnings,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format BIRD configuration
|
||||
*
|
||||
* @param string $config
|
||||
* @return string
|
||||
*/
|
||||
public static function formatConfig(string $config): string {
|
||||
$lines = explode("\n", $config);
|
||||
$output = [];
|
||||
$indent = 0;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$trimmed = trim($line);
|
||||
|
||||
// Decrease indent for closing braces
|
||||
if (preg_match('/^}/', $trimmed)) {
|
||||
$indent = max(0, $indent - 1);
|
||||
}
|
||||
|
||||
// Add line with proper indentation
|
||||
if (!empty($trimmed)) {
|
||||
$output[] = str_repeat("\t", $indent) . $trimmed;
|
||||
} else {
|
||||
$output[] = '';
|
||||
}
|
||||
|
||||
// Increase indent for opening braces
|
||||
if (preg_match('/{\s*$/', $trimmed)) {
|
||||
$indent++;
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\n", $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of BIRD 3 protocol types
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getProtocolTypes(): array {
|
||||
return [
|
||||
'bgp' => 'Border Gateway Protocol',
|
||||
'ospf' => 'Open Shortest Path First',
|
||||
'rip' => 'Routing Information Protocol',
|
||||
'babel' => 'Babel Routing Protocol',
|
||||
'static' => 'Static Routes',
|
||||
'kernel' => 'Kernel Route Table',
|
||||
'device' => 'Device Protocol',
|
||||
'direct' => 'Direct Interfaces',
|
||||
'pipe' => 'Table Pipe',
|
||||
'bfd' => 'Bidirectional Forwarding Detection',
|
||||
'rpki' => 'Resource Public Key Infrastructure',
|
||||
'mrt' => 'MRT Table Dump',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get BGP session states
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getBgpStates(): array {
|
||||
return [
|
||||
'Idle' => 'Not running',
|
||||
'Connect' => 'Connecting to peer',
|
||||
'Active' => 'Trying to connect',
|
||||
'OpenSent' => 'Open message sent',
|
||||
'OpenConfirm' => 'Waiting for keepalive',
|
||||
'Established' => 'Session established',
|
||||
];
|
||||
}
|
||||
}
|
||||
218
pathvector-admin/lib/FlatFileDB.php
Normal file
218
pathvector-admin/lib/FlatFileDB.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
/**
|
||||
* FlatFileDB Class
|
||||
*
|
||||
* Handles all flat-file JSON storage operations for the Pathvector admin dashboard
|
||||
*/
|
||||
|
||||
class FlatFileDB {
|
||||
private string $filePath;
|
||||
private array $data;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $filePath Path to the JSON file
|
||||
*/
|
||||
public function __construct(string $filePath) {
|
||||
$this->filePath = $filePath;
|
||||
$this->load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load data from JSON file
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function load(): void {
|
||||
if (file_exists($this->filePath)) {
|
||||
$content = file_get_contents($this->filePath);
|
||||
$this->data = json_decode($content, true) ?? [];
|
||||
} else {
|
||||
$this->data = [];
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data to JSON file
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function save(): bool {
|
||||
$dir = dirname($this->filePath);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$json = json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
return file_put_contents($this->filePath, $json) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all data
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAll(): array {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data by key
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $key) {
|
||||
return $this->data[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data by key
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function set(string $key, $value): bool {
|
||||
$this->data[$key] = $value;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete data by key
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(string $key): bool {
|
||||
if (isset($this->data[$key])) {
|
||||
unset($this->data[$key]);
|
||||
return $this->save();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key exists
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(string $key): bool {
|
||||
return isset($this->data[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keys
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function keys(): array {
|
||||
return array_keys($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count all entries
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int {
|
||||
return count($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function clear(): bool {
|
||||
$this->data = [];
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search data by callback function
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return array
|
||||
*/
|
||||
public function search(callable $callback): array {
|
||||
return array_filter($this->data, $callback, ARRAY_FILTER_USE_BOTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update data by key with callback
|
||||
*
|
||||
* @param string $key
|
||||
* @param callable $callback
|
||||
* @return bool
|
||||
*/
|
||||
public function update(string $key, callable $callback): bool {
|
||||
if (isset($this->data[$key])) {
|
||||
$this->data[$key] = $callback($this->data[$key]);
|
||||
return $this->save();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFilePath(): string {
|
||||
return $this->filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload data from file
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reload(): void {
|
||||
$this->load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup current data
|
||||
*
|
||||
* @param string $backupDir
|
||||
* @return string|false Returns backup file path on success, false on failure
|
||||
*/
|
||||
public function backup(string $backupDir): string|false {
|
||||
if (!is_dir($backupDir)) {
|
||||
mkdir($backupDir, 0755, true);
|
||||
}
|
||||
|
||||
$timestamp = date('Y-m-d_H-i-s');
|
||||
$filename = basename($this->filePath, '.json');
|
||||
$backupPath = $backupDir . '/' . $filename . '_' . $timestamp . '.json';
|
||||
|
||||
$json = json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
return file_put_contents($backupPath, $json) !== false ? $backupPath : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore data from backup file
|
||||
*
|
||||
* @param string $backupPath
|
||||
* @return bool
|
||||
*/
|
||||
public function restore(string $backupPath): bool {
|
||||
if (!file_exists($backupPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$content = file_get_contents($backupPath);
|
||||
$data = json_decode($content, true);
|
||||
|
||||
if ($data === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->data = $data;
|
||||
return $this->save();
|
||||
}
|
||||
}
|
||||
559
pathvector-admin/lib/Host.php
Normal file
559
pathvector-admin/lib/Host.php
Normal file
@@ -0,0 +1,559 @@
|
||||
<?php
|
||||
/**
|
||||
* Host Class
|
||||
*
|
||||
* Manages execution hosts for Pathvector operations
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/FlatFileDB.php';
|
||||
require_once __DIR__ . '/Logger.php';
|
||||
require_once __DIR__ . '/Validator.php';
|
||||
|
||||
class Host {
|
||||
private FlatFileDB $db;
|
||||
private Logger $logger;
|
||||
private Validator $validator;
|
||||
|
||||
// Execution methods
|
||||
public const METHOD_LOCAL = 'local';
|
||||
public const METHOD_SSH = 'ssh';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $dataFile Path to hosts JSON file
|
||||
* @param Logger $logger Logger instance
|
||||
*/
|
||||
public function __construct(string $dataFile, Logger $logger) {
|
||||
$this->db = new FlatFileDB($dataFile);
|
||||
$this->logger = $logger;
|
||||
$this->validator = new Validator();
|
||||
|
||||
if (!$this->db->exists('hosts')) {
|
||||
$this->db->set('hosts', []);
|
||||
$this->initializeLocalHost();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize localhost entry
|
||||
*/
|
||||
private function initializeLocalHost(): void {
|
||||
$hosts = [
|
||||
'localhost' => [
|
||||
'id' => 'localhost',
|
||||
'name' => 'Local Host',
|
||||
'hostname' => 'localhost',
|
||||
'description' => 'Local machine',
|
||||
'execution' => [
|
||||
'method' => self::METHOD_LOCAL,
|
||||
'pathvector_bin' => '/usr/local/bin/pathvector',
|
||||
'bird_bin' => '/usr/sbin/bird',
|
||||
'birdc_bin' => '/usr/sbin/birdc',
|
||||
'bird_socket' => '/var/run/bird.ctl',
|
||||
'config_dir' => '/etc/pathvector',
|
||||
'bird_dir' => '/etc/bird',
|
||||
'sudo' => true,
|
||||
],
|
||||
'is_active' => true,
|
||||
'is_default' => true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
],
|
||||
];
|
||||
|
||||
$this->db->set('hosts', $hosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all hosts
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAll(): array {
|
||||
return $this->db->get('hosts') ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get host by ID
|
||||
*
|
||||
* @param string $id
|
||||
* @return array|null
|
||||
*/
|
||||
public function get(string $id): ?array {
|
||||
$hosts = $this->db->get('hosts') ?? [];
|
||||
return $hosts[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default host
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getDefault(): ?array {
|
||||
$hosts = $this->db->get('hosts') ?? [];
|
||||
|
||||
foreach ($hosts as $host) {
|
||||
if ($host['is_default'] ?? false) {
|
||||
return $host;
|
||||
}
|
||||
}
|
||||
|
||||
// Return first active host if no default
|
||||
foreach ($hosts as $host) {
|
||||
if ($host['is_active'] ?? false) {
|
||||
return $host;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new host
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function create(array $data): array {
|
||||
if (empty($data['id'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Host ID is required',
|
||||
'errors' => ['id' => 'Host ID is required'],
|
||||
];
|
||||
}
|
||||
|
||||
$hosts = $this->db->get('hosts') ?? [];
|
||||
$id = $this->sanitizeId($data['id']);
|
||||
|
||||
if (isset($hosts[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Host $id already exists",
|
||||
'errors' => ['id' => 'Host ID already exists'],
|
||||
];
|
||||
}
|
||||
|
||||
// Validate SSH settings if SSH method
|
||||
$method = $data['execution']['method'] ?? self::METHOD_LOCAL;
|
||||
|
||||
if ($method === self::METHOD_SSH) {
|
||||
if (empty($data['execution']['ssh_host'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'SSH host is required for SSH execution method',
|
||||
'errors' => ['ssh_host' => 'SSH host is required'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$hostData = [
|
||||
'id' => $id,
|
||||
'name' => $data['name'] ?? $id,
|
||||
'hostname' => $data['hostname'] ?? '',
|
||||
'description' => $data['description'] ?? '',
|
||||
'execution' => $this->buildExecutionConfig($data['execution'] ?? []),
|
||||
'metadata' => $data['metadata'] ?? [],
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
'is_default' => false, // New hosts are not default
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
];
|
||||
|
||||
$hosts[$id] = $hostData;
|
||||
|
||||
if ($this->db->set('hosts', $hosts)) {
|
||||
$this->logger->success('host', "Created host: $id");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Host $id created successfully",
|
||||
'data' => $hostData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to save host',
|
||||
'errors' => ['database' => 'Failed to save host'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update host
|
||||
*
|
||||
* @param string $id
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function update(string $id, array $data): array {
|
||||
$hosts = $this->db->get('hosts') ?? [];
|
||||
|
||||
if (!isset($hosts[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Host $id not found",
|
||||
'errors' => ['id' => 'Host not found'],
|
||||
];
|
||||
}
|
||||
|
||||
$hostData = $hosts[$id];
|
||||
|
||||
$allowedFields = ['name', 'hostname', 'description', 'metadata', 'is_active'];
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$hostData[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['execution'])) {
|
||||
$hostData['execution'] = $this->buildExecutionConfig(
|
||||
array_merge($hostData['execution'], $data['execution'])
|
||||
);
|
||||
}
|
||||
|
||||
$hostData['updated_at'] = date('c');
|
||||
$hosts[$id] = $hostData;
|
||||
|
||||
if ($this->db->set('hosts', $hosts)) {
|
||||
$this->logger->info('host', "Updated host: $id");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Host $id updated successfully",
|
||||
'data' => $hostData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to update host',
|
||||
'errors' => ['database' => 'Failed to save host'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete host
|
||||
*
|
||||
* @param string $id
|
||||
* @return array
|
||||
*/
|
||||
public function delete(string $id): array {
|
||||
$hosts = $this->db->get('hosts') ?? [];
|
||||
|
||||
if (!isset($hosts[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Host $id not found",
|
||||
];
|
||||
}
|
||||
|
||||
if ($id === 'localhost') {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Cannot delete localhost',
|
||||
];
|
||||
}
|
||||
|
||||
if ($hosts[$id]['is_default'] ?? false) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Cannot delete default host. Set another host as default first.',
|
||||
];
|
||||
}
|
||||
|
||||
$hostName = $hosts[$id]['name'];
|
||||
unset($hosts[$id]);
|
||||
|
||||
if ($this->db->set('hosts', $hosts)) {
|
||||
$this->logger->warning('host', "Deleted host: $id ($hostName)");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Host $id deleted successfully",
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to delete host',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default host
|
||||
*
|
||||
* @param string $id
|
||||
* @return array
|
||||
*/
|
||||
public function setDefault(string $id): array {
|
||||
$hosts = $this->db->get('hosts') ?? [];
|
||||
|
||||
if (!isset($hosts[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Host $id not found",
|
||||
];
|
||||
}
|
||||
|
||||
// Unset previous default
|
||||
foreach ($hosts as $hostId => $host) {
|
||||
$hosts[$hostId]['is_default'] = ($hostId === $id);
|
||||
}
|
||||
|
||||
if ($this->db->set('hosts', $hosts)) {
|
||||
$this->logger->info('host', "Set default host: $id");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Host $id set as default",
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to set default host',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build execution config structure
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
private function buildExecutionConfig(array $data): array {
|
||||
return [
|
||||
'method' => $data['method'] ?? self::METHOD_LOCAL,
|
||||
|
||||
// Binary paths
|
||||
'pathvector_bin' => $data['pathvector_bin'] ?? '/usr/local/bin/pathvector',
|
||||
'bird_bin' => $data['bird_bin'] ?? '/usr/sbin/bird',
|
||||
'birdc_bin' => $data['birdc_bin'] ?? '/usr/sbin/birdc',
|
||||
|
||||
// Paths
|
||||
'bird_socket' => $data['bird_socket'] ?? '/var/run/bird.ctl',
|
||||
'config_dir' => $data['config_dir'] ?? '/etc/pathvector',
|
||||
'bird_dir' => $data['bird_dir'] ?? '/etc/bird',
|
||||
'cache_dir' => $data['cache_dir'] ?? '/var/cache/pathvector',
|
||||
|
||||
// Permissions
|
||||
'sudo' => $data['sudo'] ?? true,
|
||||
|
||||
// SSH settings (for SSH method)
|
||||
'ssh_host' => $data['ssh_host'] ?? '',
|
||||
'ssh_port' => $data['ssh_port'] ?? 22,
|
||||
'ssh_user' => $data['ssh_user'] ?? 'root',
|
||||
'ssh_key' => $data['ssh_key'] ?? '',
|
||||
'ssh_options' => $data['ssh_options'] ?? '-o StrictHostKeyChecking=no',
|
||||
|
||||
// Timeouts
|
||||
'timeout' => $data['timeout'] ?? 30,
|
||||
'connect_timeout' => $data['connect_timeout'] ?? 10,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test host connectivity
|
||||
*
|
||||
* @param string $id
|
||||
* @return array
|
||||
*/
|
||||
public function testConnection(string $id): array {
|
||||
$host = $this->get($id);
|
||||
|
||||
if (!$host) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Host $id not found",
|
||||
];
|
||||
}
|
||||
|
||||
$exec = $host['execution'];
|
||||
|
||||
if ($exec['method'] === self::METHOD_LOCAL) {
|
||||
// Test local pathvector binary
|
||||
$output = [];
|
||||
$returnCode = 0;
|
||||
|
||||
$cmd = escapeshellcmd($exec['pathvector_bin']) . ' version';
|
||||
if ($exec['sudo']) {
|
||||
$cmd = 'sudo ' . $cmd;
|
||||
}
|
||||
|
||||
exec($cmd . ' 2>&1', $output, $returnCode);
|
||||
|
||||
if ($returnCode === 0) {
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Local host connection successful',
|
||||
'output' => implode("\n", $output),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to execute pathvector',
|
||||
'output' => implode("\n", $output),
|
||||
];
|
||||
}
|
||||
} elseif ($exec['method'] === self::METHOD_SSH) {
|
||||
// Test SSH connection
|
||||
$sshCmd = $this->buildSshCommand($exec, 'echo "Connection successful" && ' . escapeshellcmd($exec['pathvector_bin']) . ' version');
|
||||
|
||||
$output = [];
|
||||
$returnCode = 0;
|
||||
exec($sshCmd . ' 2>&1', $output, $returnCode);
|
||||
|
||||
if ($returnCode === 0) {
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'SSH connection successful',
|
||||
'output' => implode("\n", $output),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'SSH connection failed',
|
||||
'output' => implode("\n", $output),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Unknown execution method',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build SSH command
|
||||
*
|
||||
* @param array $exec Execution config
|
||||
* @param string $remoteCmd Command to run on remote host
|
||||
* @return string
|
||||
*/
|
||||
public function buildSshCommand(array $exec, string $remoteCmd): string {
|
||||
$ssh = 'ssh';
|
||||
|
||||
if (!empty($exec['ssh_key'])) {
|
||||
$ssh .= ' -i ' . escapeshellarg($exec['ssh_key']);
|
||||
}
|
||||
|
||||
if (!empty($exec['ssh_port']) && $exec['ssh_port'] != 22) {
|
||||
$ssh .= ' -p ' . (int) $exec['ssh_port'];
|
||||
}
|
||||
|
||||
if (!empty($exec['ssh_options'])) {
|
||||
$ssh .= ' ' . $exec['ssh_options'];
|
||||
}
|
||||
|
||||
$ssh .= ' -o ConnectTimeout=' . (int) ($exec['connect_timeout'] ?? 10);
|
||||
|
||||
$target = escapeshellarg($exec['ssh_user'] . '@' . $exec['ssh_host']);
|
||||
|
||||
if ($exec['sudo'] ?? false) {
|
||||
$remoteCmd = 'sudo ' . $remoteCmd;
|
||||
}
|
||||
|
||||
return $ssh . ' ' . $target . ' ' . escapeshellarg($remoteCmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute command on host
|
||||
*
|
||||
* @param string $id Host ID
|
||||
* @param string $command Command to execute
|
||||
* @return array
|
||||
*/
|
||||
public function executeCommand(string $id, string $command): array {
|
||||
$host = $this->get($id);
|
||||
|
||||
if (!$host) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Host $id not found",
|
||||
'output' => '',
|
||||
];
|
||||
}
|
||||
|
||||
if (!($host['is_active'] ?? false)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Host $id is not active",
|
||||
'output' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$exec = $host['execution'];
|
||||
$output = [];
|
||||
$returnCode = 0;
|
||||
|
||||
if ($exec['method'] === self::METHOD_LOCAL) {
|
||||
$cmd = $command;
|
||||
if ($exec['sudo'] ?? false) {
|
||||
$cmd = 'sudo ' . $cmd;
|
||||
}
|
||||
|
||||
exec($cmd . ' 2>&1', $output, $returnCode);
|
||||
} elseif ($exec['method'] === self::METHOD_SSH) {
|
||||
$sshCmd = $this->buildSshCommand($exec, $command);
|
||||
exec($sshCmd . ' 2>&1', $output, $returnCode);
|
||||
} else {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Unknown execution method',
|
||||
'output' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$this->logger->info('host', "Executed command on $id: $command", ['return_code' => $returnCode]);
|
||||
|
||||
return [
|
||||
'success' => $returnCode === 0,
|
||||
'message' => $returnCode === 0 ? 'Command executed successfully' : 'Command failed',
|
||||
'output' => implode("\n", $output),
|
||||
'return_code' => $returnCode,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize host ID
|
||||
*
|
||||
* @param string $id
|
||||
* @return string
|
||||
*/
|
||||
private function sanitizeId(string $id): string {
|
||||
$id = strtolower(trim($id));
|
||||
$id = preg_replace('/\s+/', '-', $id);
|
||||
$id = preg_replace('/[^a-z0-9-_]/', '', $id);
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count hosts
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int {
|
||||
$hosts = $this->db->get('hosts') ?? [];
|
||||
return count($hosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active hosts
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getActive(): array {
|
||||
$hosts = $this->db->get('hosts') ?? [];
|
||||
return array_filter($hosts, fn($h) => $h['is_active'] ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup hosts data
|
||||
*
|
||||
* @param string $backupDir
|
||||
* @return string|false
|
||||
*/
|
||||
public function backup(string $backupDir): string|false {
|
||||
return $this->db->backup($backupDir);
|
||||
}
|
||||
}
|
||||
285
pathvector-admin/lib/Logger.php
Normal file
285
pathvector-admin/lib/Logger.php
Normal file
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
/**
|
||||
* Logger Class
|
||||
*
|
||||
* Handles audit logging for the Pathvector admin dashboard
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/FlatFileDB.php';
|
||||
|
||||
class Logger {
|
||||
private FlatFileDB $db;
|
||||
private int $maxEntries;
|
||||
private array $validLevels = ['info', 'warning', 'error', 'success'];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $logFile Path to the log file
|
||||
* @param int $maxEntries Maximum number of log entries to keep
|
||||
*/
|
||||
public function __construct(string $logFile, int $maxEntries = 1000) {
|
||||
$this->db = new FlatFileDB($logFile);
|
||||
$this->maxEntries = $maxEntries;
|
||||
|
||||
// Initialize logs array if not exists
|
||||
if (!$this->db->exists('logs')) {
|
||||
$this->db->set('logs', []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a log entry
|
||||
*
|
||||
* @param string $level Log level (info, warning, error, success)
|
||||
* @param string $action Action performed
|
||||
* @param string $message Log message
|
||||
* @param string|null $user User who performed the action
|
||||
* @param array $context Additional context data
|
||||
* @return bool
|
||||
*/
|
||||
public function log(string $level, string $action, string $message, ?string $user = null, array $context = []): bool {
|
||||
if (!in_array($level, $this->validLevels)) {
|
||||
$level = 'info';
|
||||
}
|
||||
|
||||
$logs = $this->db->get('logs') ?? [];
|
||||
|
||||
$entry = [
|
||||
'id' => uniqid('log_'),
|
||||
'timestamp' => date('c'),
|
||||
'level' => $level,
|
||||
'action' => $action,
|
||||
'message' => $message,
|
||||
'user' => $user ?? ($_SESSION['user']['username'] ?? 'system'),
|
||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
||||
'context' => $context,
|
||||
];
|
||||
|
||||
array_unshift($logs, $entry);
|
||||
|
||||
// Trim logs to max entries
|
||||
if (count($logs) > $this->maxEntries) {
|
||||
$logs = array_slice($logs, 0, $this->maxEntries);
|
||||
}
|
||||
|
||||
return $this->db->set('logs', $logs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log info level message
|
||||
*
|
||||
* @param string $action
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return bool
|
||||
*/
|
||||
public function info(string $action, string $message, array $context = []): bool {
|
||||
return $this->log('info', $action, $message, null, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning level message
|
||||
*
|
||||
* @param string $action
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return bool
|
||||
*/
|
||||
public function warning(string $action, string $message, array $context = []): bool {
|
||||
return $this->log('warning', $action, $message, null, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error level message
|
||||
*
|
||||
* @param string $action
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return bool
|
||||
*/
|
||||
public function error(string $action, string $message, array $context = []): bool {
|
||||
return $this->log('error', $action, $message, null, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log success level message
|
||||
*
|
||||
* @param string $action
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return bool
|
||||
*/
|
||||
public function success(string $action, string $message, array $context = []): bool {
|
||||
return $this->log('success', $action, $message, null, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all logs
|
||||
*
|
||||
* @param int|null $limit
|
||||
* @param int $offset
|
||||
* @return array
|
||||
*/
|
||||
public function getLogs(?int $limit = null, int $offset = 0): array {
|
||||
$logs = $this->db->get('logs') ?? [];
|
||||
|
||||
if ($limit !== null) {
|
||||
return array_slice($logs, $offset, $limit);
|
||||
}
|
||||
|
||||
return $logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logs by level
|
||||
*
|
||||
* @param string $level
|
||||
* @param int|null $limit
|
||||
* @return array
|
||||
*/
|
||||
public function getByLevel(string $level, ?int $limit = null): array {
|
||||
$logs = $this->db->get('logs') ?? [];
|
||||
$filtered = array_filter($logs, fn($log) => $log['level'] === $level);
|
||||
|
||||
if ($limit !== null) {
|
||||
return array_slice($filtered, 0, $limit);
|
||||
}
|
||||
|
||||
return array_values($filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logs by user
|
||||
*
|
||||
* @param string $user
|
||||
* @param int|null $limit
|
||||
* @return array
|
||||
*/
|
||||
public function getByUser(string $user, ?int $limit = null): array {
|
||||
$logs = $this->db->get('logs') ?? [];
|
||||
$filtered = array_filter($logs, fn($log) => $log['user'] === $user);
|
||||
|
||||
if ($limit !== null) {
|
||||
return array_slice($filtered, 0, $limit);
|
||||
}
|
||||
|
||||
return array_values($filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logs by action
|
||||
*
|
||||
* @param string $action
|
||||
* @param int|null $limit
|
||||
* @return array
|
||||
*/
|
||||
public function getByAction(string $action, ?int $limit = null): array {
|
||||
$logs = $this->db->get('logs') ?? [];
|
||||
$filtered = array_filter($logs, fn($log) => strpos($log['action'], $action) !== false);
|
||||
|
||||
if ($limit !== null) {
|
||||
return array_slice($filtered, 0, $limit);
|
||||
}
|
||||
|
||||
return array_values($filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logs by date range
|
||||
*
|
||||
* @param string $startDate
|
||||
* @param string $endDate
|
||||
* @return array
|
||||
*/
|
||||
public function getByDateRange(string $startDate, string $endDate): array {
|
||||
$logs = $this->db->get('logs') ?? [];
|
||||
$start = strtotime($startDate);
|
||||
$end = strtotime($endDate);
|
||||
|
||||
return array_values(array_filter($logs, function($log) use ($start, $end) {
|
||||
$logTime = strtotime($log['timestamp']);
|
||||
return $logTime >= $start && $logTime <= $end;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Search logs
|
||||
*
|
||||
* @param string $query
|
||||
* @param int|null $limit
|
||||
* @return array
|
||||
*/
|
||||
public function search(string $query, ?int $limit = null): array {
|
||||
$logs = $this->db->get('logs') ?? [];
|
||||
$query = strtolower($query);
|
||||
|
||||
$filtered = array_filter($logs, function($log) use ($query) {
|
||||
return strpos(strtolower($log['message']), $query) !== false ||
|
||||
strpos(strtolower($log['action']), $query) !== false ||
|
||||
strpos(strtolower($log['user']), $query) !== false;
|
||||
});
|
||||
|
||||
if ($limit !== null) {
|
||||
return array_slice($filtered, 0, $limit);
|
||||
}
|
||||
|
||||
return array_values($filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total log count
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int {
|
||||
$logs = $this->db->get('logs') ?? [];
|
||||
return count($logs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all logs
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function clear(): bool {
|
||||
return $this->db->set('logs', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export logs to JSON
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function exportJson(): string {
|
||||
$logs = $this->db->get('logs') ?? [];
|
||||
return json_encode($logs, JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export logs to CSV
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function exportCsv(): string {
|
||||
$logs = $this->db->get('logs') ?? [];
|
||||
|
||||
$output = "ID,Timestamp,Level,Action,Message,User,IP\n";
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$output .= sprintf(
|
||||
'"%s","%s","%s","%s","%s","%s","%s"' . "\n",
|
||||
$log['id'],
|
||||
$log['timestamp'],
|
||||
$log['level'],
|
||||
str_replace('"', '""', $log['action']),
|
||||
str_replace('"', '""', $log['message']),
|
||||
$log['user'],
|
||||
$log['ip']
|
||||
);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
516
pathvector-admin/lib/Node.php
Normal file
516
pathvector-admin/lib/Node.php
Normal file
@@ -0,0 +1,516 @@
|
||||
<?php
|
||||
/**
|
||||
* Node Class
|
||||
*
|
||||
* Manages router/node configurations within ASNs
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/FlatFileDB.php';
|
||||
require_once __DIR__ . '/Logger.php';
|
||||
require_once __DIR__ . '/Validator.php';
|
||||
|
||||
class Node {
|
||||
private FlatFileDB $db;
|
||||
private Logger $logger;
|
||||
private Validator $validator;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $dataFile Path to nodes JSON file
|
||||
* @param Logger $logger Logger instance
|
||||
*/
|
||||
public function __construct(string $dataFile, Logger $logger) {
|
||||
$this->db = new FlatFileDB($dataFile);
|
||||
$this->logger = $logger;
|
||||
$this->validator = new Validator();
|
||||
|
||||
if (!$this->db->exists('nodes')) {
|
||||
$this->db->set('nodes', []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all nodes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAll(): array {
|
||||
return $this->db->get('nodes') ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nodes by ASN
|
||||
*
|
||||
* @param int $asn
|
||||
* @return array
|
||||
*/
|
||||
public function getByAsn(int $asn): array {
|
||||
$nodes = $this->db->get('nodes') ?? [];
|
||||
return array_filter($nodes, fn($node) => ($node['asn'] ?? 0) === $asn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node by ID
|
||||
*
|
||||
* @param string $id
|
||||
* @return array|null
|
||||
*/
|
||||
public function get(string $id): ?array {
|
||||
$nodes = $this->db->get('nodes') ?? [];
|
||||
return $nodes[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new node
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function create(array $data): array {
|
||||
$this->validator->clear();
|
||||
|
||||
// Validate required fields
|
||||
if (empty($data['id'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Node ID is required',
|
||||
'errors' => ['id' => 'Node ID is required'],
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($data['asn'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'ASN is required',
|
||||
'errors' => ['asn' => 'ASN is required'],
|
||||
];
|
||||
}
|
||||
|
||||
if (!$this->validator->validateAsn($data['asn'], 'asn')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Invalid ASN',
|
||||
'errors' => $this->validator->getErrors(),
|
||||
];
|
||||
}
|
||||
|
||||
$nodes = $this->db->get('nodes') ?? [];
|
||||
$id = $this->sanitizeId($data['id']);
|
||||
|
||||
if (isset($nodes[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Node $id already exists",
|
||||
'errors' => ['id' => 'Node ID already exists'],
|
||||
];
|
||||
}
|
||||
|
||||
// Validate optional fields
|
||||
if (!empty($data['router_id']) && !$this->validator->validateRouterId($data['router_id'], 'router_id')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Invalid router ID',
|
||||
'errors' => $this->validator->getErrors(),
|
||||
];
|
||||
}
|
||||
|
||||
$nodeData = [
|
||||
'id' => $id,
|
||||
'asn' => (int) $data['asn'],
|
||||
'name' => $data['name'] ?? $id,
|
||||
'hostname' => $data['hostname'] ?? '',
|
||||
'router_id' => $data['router_id'] ?? '',
|
||||
'description' => $data['description'] ?? '',
|
||||
'location' => $data['location'] ?? '',
|
||||
'pathvector' => $this->buildPathvectorConfig($data['pathvector'] ?? []),
|
||||
'host' => $data['host'] ?? '',
|
||||
'templates' => $data['templates'] ?? [],
|
||||
'overrides' => $data['overrides'] ?? [],
|
||||
'metadata' => $data['metadata'] ?? [],
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
];
|
||||
|
||||
$nodes[$id] = $nodeData;
|
||||
|
||||
if ($this->db->set('nodes', $nodes)) {
|
||||
$this->logger->success('node', "Created node: $id for ASN {$nodeData['asn']}");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Node $id created successfully",
|
||||
'data' => $nodeData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to save node',
|
||||
'errors' => ['database' => 'Failed to save node'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update node
|
||||
*
|
||||
* @param string $id
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function update(string $id, array $data): array {
|
||||
$nodes = $this->db->get('nodes') ?? [];
|
||||
|
||||
if (!isset($nodes[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Node $id not found",
|
||||
'errors' => ['id' => 'Node not found'],
|
||||
];
|
||||
}
|
||||
|
||||
$nodeData = $nodes[$id];
|
||||
|
||||
// Update allowed fields
|
||||
$allowedFields = [
|
||||
'name', 'hostname', 'router_id', 'description', 'location',
|
||||
'host', 'templates', 'overrides', 'metadata', 'is_active'
|
||||
];
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$nodeData[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
|
||||
// Update Pathvector config
|
||||
if (isset($data['pathvector'])) {
|
||||
$nodeData['pathvector'] = $this->buildPathvectorConfig(
|
||||
array_merge($nodeData['pathvector'], $data['pathvector'])
|
||||
);
|
||||
}
|
||||
|
||||
// Validate router_id if changed
|
||||
if (!empty($nodeData['router_id']) && !$this->validator->validateRouterId($nodeData['router_id'], 'router_id')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Invalid router ID',
|
||||
'errors' => $this->validator->getErrors(),
|
||||
];
|
||||
}
|
||||
|
||||
$nodeData['updated_at'] = date('c');
|
||||
$nodes[$id] = $nodeData;
|
||||
|
||||
if ($this->db->set('nodes', $nodes)) {
|
||||
$this->logger->info('node', "Updated node: $id");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Node $id updated successfully",
|
||||
'data' => $nodeData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to update node',
|
||||
'errors' => ['database' => 'Failed to save node'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete node
|
||||
*
|
||||
* @param string $id
|
||||
* @return array
|
||||
*/
|
||||
public function delete(string $id): array {
|
||||
$nodes = $this->db->get('nodes') ?? [];
|
||||
|
||||
if (!isset($nodes[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Node $id not found",
|
||||
];
|
||||
}
|
||||
|
||||
$nodeName = $nodes[$id]['name'];
|
||||
$nodeAsn = $nodes[$id]['asn'];
|
||||
unset($nodes[$id]);
|
||||
|
||||
if ($this->db->set('nodes', $nodes)) {
|
||||
$this->logger->warning('node', "Deleted node: $id ($nodeName) from ASN $nodeAsn");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Node $id deleted successfully",
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to delete node',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Pathvector config structure for a node
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
private function buildPathvectorConfig(array $data): array {
|
||||
return [
|
||||
'binary_path' => $data['binary_path'] ?? '/usr/local/bin/pathvector',
|
||||
'config_path' => $data['config_path'] ?? '/etc/pathvector/pathvector.yml',
|
||||
'output_path' => $data['output_path'] ?? '/etc/bird/bird.conf',
|
||||
'bird_reload_cmd' => $data['bird_reload_cmd'] ?? 'birdc configure',
|
||||
'bird_directory' => $data['bird_directory'] ?? '/etc/bird',
|
||||
'bird_socket' => $data['bird_socket'] ?? '/var/run/bird.ctl',
|
||||
'cache_directory' => $data['cache_directory'] ?? '/var/cache/pathvector',
|
||||
|
||||
// Node-specific overrides
|
||||
'source4' => $data['source4'] ?? '',
|
||||
'source6' => $data['source6'] ?? '',
|
||||
'prefixes' => $data['prefixes'] ?? [],
|
||||
|
||||
// RPKI settings
|
||||
'rpki_enable' => $data['rpki_enable'] ?? true,
|
||||
'rtr_server' => $data['rtr_server'] ?? '',
|
||||
|
||||
// PeeringDB settings
|
||||
'peeringdb_api_key' => $data['peeringdb_api_key'] ?? '',
|
||||
'peeringdb_cache' => $data['peeringdb_cache'] ?? true,
|
||||
|
||||
// IRR settings
|
||||
'irr_server' => $data['irr_server'] ?? 'rr.ntt.net',
|
||||
|
||||
// Kernel settings
|
||||
'kernel_table' => $data['kernel_table'] ?? null,
|
||||
'kernel_learn' => $data['kernel_learn'] ?? false,
|
||||
'kernel_export' => $data['kernel_export'] ?? [],
|
||||
|
||||
// Global config snippet
|
||||
'global_config' => $data['global_config'] ?? '',
|
||||
|
||||
// Web UI
|
||||
'web_ui_file' => $data['web_ui_file'] ?? '',
|
||||
|
||||
// Logging
|
||||
'log_file' => $data['log_file'] ?? 'syslog',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize node ID
|
||||
*
|
||||
* @param string $id
|
||||
* @return string
|
||||
*/
|
||||
private function sanitizeId(string $id): string {
|
||||
// Convert to lowercase, replace spaces with hyphens, remove invalid characters
|
||||
$id = strtolower(trim($id));
|
||||
$id = preg_replace('/\s+/', '-', $id);
|
||||
$id = preg_replace('/[^a-z0-9-_]/', '', $id);
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count nodes
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int {
|
||||
$nodes = $this->db->get('nodes') ?? [];
|
||||
return count($nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count nodes by ASN
|
||||
*
|
||||
* @param int $asn
|
||||
* @return int
|
||||
*/
|
||||
public function countByAsn(int $asn): int {
|
||||
return count($this->getByAsn($asn));
|
||||
}
|
||||
|
||||
/**
|
||||
* Search nodes
|
||||
*
|
||||
* @param string $query
|
||||
* @return array
|
||||
*/
|
||||
public function search(string $query): array {
|
||||
$nodes = $this->db->get('nodes') ?? [];
|
||||
$query = strtolower($query);
|
||||
|
||||
return array_filter($nodes, function($node) use ($query) {
|
||||
return strpos(strtolower($node['id']), $query) !== false ||
|
||||
strpos(strtolower($node['name']), $query) !== false ||
|
||||
strpos(strtolower($node['hostname'] ?? ''), $query) !== false ||
|
||||
strpos(strtolower($node['description'] ?? ''), $query) !== false ||
|
||||
strpos(strtolower($node['location'] ?? ''), $query) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nodes by host
|
||||
*
|
||||
* @param string $hostId
|
||||
* @return array
|
||||
*/
|
||||
public function getByHost(string $hostId): array {
|
||||
$nodes = $this->db->get('nodes') ?? [];
|
||||
return array_filter($nodes, fn($node) => ($node['host'] ?? '') === $hostId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export node configuration to YAML
|
||||
*
|
||||
* @param string $id
|
||||
* @param ASN $asnManager ASN manager for defaults
|
||||
* @return string|null
|
||||
*/
|
||||
public function exportYaml(string $id, $asnManager = null): ?string {
|
||||
$node = $this->get($id);
|
||||
|
||||
if (!$node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$config = [];
|
||||
$config['asn'] = $node['asn'];
|
||||
|
||||
if (!empty($node['router_id'])) {
|
||||
$config['router-id'] = $node['router_id'];
|
||||
}
|
||||
|
||||
if (!empty($node['hostname'])) {
|
||||
$config['hostname'] = $node['hostname'];
|
||||
}
|
||||
|
||||
// Pathvector settings
|
||||
$pv = $node['pathvector'];
|
||||
|
||||
if (!empty($pv['source4'])) {
|
||||
$config['source4'] = $pv['source4'];
|
||||
}
|
||||
|
||||
if (!empty($pv['source6'])) {
|
||||
$config['source6'] = $pv['source6'];
|
||||
}
|
||||
|
||||
if (!empty($pv['prefixes'])) {
|
||||
$config['prefixes'] = $pv['prefixes'];
|
||||
}
|
||||
|
||||
if (!empty($pv['bird_directory'])) {
|
||||
$config['bird-directory'] = $pv['bird_directory'];
|
||||
}
|
||||
|
||||
if (!empty($pv['bird_socket'])) {
|
||||
$config['bird-socket'] = $pv['bird_socket'];
|
||||
}
|
||||
|
||||
if (!empty($pv['cache_directory'])) {
|
||||
$config['cache-directory'] = $pv['cache_directory'];
|
||||
}
|
||||
|
||||
if (isset($pv['rpki_enable'])) {
|
||||
$config['rpki-enable'] = $pv['rpki_enable'];
|
||||
}
|
||||
|
||||
if (!empty($pv['rtr_server'])) {
|
||||
$config['rtr-server'] = $pv['rtr_server'];
|
||||
}
|
||||
|
||||
if (!empty($pv['irr_server'])) {
|
||||
$config['irr-server'] = $pv['irr_server'];
|
||||
}
|
||||
|
||||
if (!empty($pv['global_config'])) {
|
||||
$config['global-config'] = $pv['global_config'];
|
||||
}
|
||||
|
||||
// Apply overrides
|
||||
foreach ($node['overrides'] as $key => $value) {
|
||||
if ($value !== '' && $value !== null) {
|
||||
$config[str_replace('_', '-', $key)] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->arrayToYaml($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert array to YAML string
|
||||
*
|
||||
* @param array $data
|
||||
* @param int $indent
|
||||
* @return string
|
||||
*/
|
||||
private function arrayToYaml(array $data, int $indent = 0): string {
|
||||
$yaml = '';
|
||||
$prefix = str_repeat(' ', $indent);
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
if (empty($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (array_keys($value) === range(0, count($value) - 1)) {
|
||||
$yaml .= "$prefix$key:\n";
|
||||
foreach ($value as $item) {
|
||||
if (is_array($item)) {
|
||||
$yaml .= "$prefix -\n" . $this->arrayToYaml($item, $indent + 2);
|
||||
} else {
|
||||
$yaml .= "$prefix - " . $this->formatYamlValue($item) . "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$yaml .= "$prefix$key:\n" . $this->arrayToYaml($value, $indent + 1);
|
||||
}
|
||||
} else {
|
||||
if ($value === '' || $value === null) {
|
||||
continue;
|
||||
}
|
||||
$yaml .= "$prefix$key: " . $this->formatYamlValue($value) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $yaml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format value for YAML output
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
private function formatYamlValue($value): string {
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
if (is_string($value) && (strpos($value, ':') !== false || strpos($value, '#') !== false || strpos($value, "\n") !== false)) {
|
||||
return '"' . addslashes($value) . '"';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup nodes data
|
||||
*
|
||||
* @param string $backupDir
|
||||
* @return string|false
|
||||
*/
|
||||
public function backup(string $backupDir): string|false {
|
||||
return $this->db->backup($backupDir);
|
||||
}
|
||||
}
|
||||
1046
pathvector-admin/lib/Pathvector.php
Normal file
1046
pathvector-admin/lib/Pathvector.php
Normal file
File diff suppressed because it is too large
Load Diff
689
pathvector-admin/lib/Peer.php
Normal file
689
pathvector-admin/lib/Peer.php
Normal file
@@ -0,0 +1,689 @@
|
||||
<?php
|
||||
/**
|
||||
* Peer Class
|
||||
*
|
||||
* Manages BGP peer configurations with full Pathvector options support
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/FlatFileDB.php';
|
||||
require_once __DIR__ . '/Logger.php';
|
||||
require_once __DIR__ . '/Validator.php';
|
||||
|
||||
class Peer {
|
||||
private FlatFileDB $db;
|
||||
private Logger $logger;
|
||||
private Validator $validator;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $dataFile Path to peers JSON file
|
||||
* @param Logger $logger Logger instance
|
||||
*/
|
||||
public function __construct(string $dataFile, Logger $logger) {
|
||||
$this->db = new FlatFileDB($dataFile);
|
||||
$this->logger = $logger;
|
||||
$this->validator = new Validator();
|
||||
|
||||
if (!$this->db->exists('peers')) {
|
||||
$this->db->set('peers', []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all peers
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAll(): array {
|
||||
return $this->db->get('peers') ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get peers by ASN
|
||||
*
|
||||
* @param int $asn
|
||||
* @return array
|
||||
*/
|
||||
public function getByAsn(int $asn): array {
|
||||
$peers = $this->db->get('peers') ?? [];
|
||||
return array_filter($peers, fn($peer) => ($peer['asn'] ?? 0) === $asn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get peers by node
|
||||
*
|
||||
* @param string $nodeId
|
||||
* @return array
|
||||
*/
|
||||
public function getByNode(string $nodeId): array {
|
||||
$peers = $this->db->get('peers') ?? [];
|
||||
return array_filter($peers, fn($peer) => ($peer['node'] ?? '') === $nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get peer by ID
|
||||
*
|
||||
* @param string $id
|
||||
* @return array|null
|
||||
*/
|
||||
public function get(string $id): ?array {
|
||||
$peers = $this->db->get('peers') ?? [];
|
||||
return $peers[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new peer
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function create(array $data): array {
|
||||
$this->validator->clear();
|
||||
|
||||
// Validate required fields
|
||||
if (empty($data['id'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Peer ID is required',
|
||||
'errors' => ['id' => 'Peer ID is required'],
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($data['asn'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Local ASN is required',
|
||||
'errors' => ['asn' => 'Local ASN is required'],
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($data['remote_asn'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Remote ASN is required',
|
||||
'errors' => ['remote_asn' => 'Remote ASN is required'],
|
||||
];
|
||||
}
|
||||
|
||||
if (!$this->validator->validateAsn($data['asn'], 'asn') ||
|
||||
!$this->validator->validateAsn($data['remote_asn'], 'remote_asn')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Invalid ASN',
|
||||
'errors' => $this->validator->getErrors(),
|
||||
];
|
||||
}
|
||||
|
||||
$peers = $this->db->get('peers') ?? [];
|
||||
$id = $this->sanitizeId($data['id']);
|
||||
|
||||
if (isset($peers[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Peer $id already exists",
|
||||
'errors' => ['id' => 'Peer ID already exists'],
|
||||
];
|
||||
}
|
||||
|
||||
// Validate neighbors
|
||||
$neighbors = $data['neighbors'] ?? [];
|
||||
if (!is_array($neighbors)) {
|
||||
$neighbors = array_filter(array_map('trim', explode("\n", $neighbors)));
|
||||
}
|
||||
|
||||
foreach ($neighbors as $neighbor) {
|
||||
if (!$this->validator->validateIP($neighbor, 'neighbors')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Invalid neighbor IP: $neighbor",
|
||||
'errors' => $this->validator->getErrors(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$peerData = $this->buildPeerData($id, $data, $neighbors);
|
||||
$peers[$id] = $peerData;
|
||||
|
||||
if ($this->db->set('peers', $peers)) {
|
||||
$this->logger->success('peer', "Created peer: $id (AS{$peerData['remote_asn']}) on node {$peerData['node']}");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Peer $id created successfully",
|
||||
'data' => $peerData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to save peer',
|
||||
'errors' => ['database' => 'Failed to save peer'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update peer
|
||||
*
|
||||
* @param string $id
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function update(string $id, array $data): array {
|
||||
$peers = $this->db->get('peers') ?? [];
|
||||
|
||||
if (!isset($peers[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Peer $id not found",
|
||||
'errors' => ['id' => 'Peer not found'],
|
||||
];
|
||||
}
|
||||
|
||||
$peerData = $peers[$id];
|
||||
|
||||
// Handle neighbors
|
||||
if (isset($data['neighbors'])) {
|
||||
$neighbors = $data['neighbors'];
|
||||
if (!is_array($neighbors)) {
|
||||
$neighbors = array_filter(array_map('trim', explode("\n", $neighbors)));
|
||||
}
|
||||
|
||||
foreach ($neighbors as $neighbor) {
|
||||
if (!$this->validator->validateIP($neighbor, 'neighbors')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Invalid neighbor IP: $neighbor",
|
||||
'errors' => $this->validator->getErrors(),
|
||||
];
|
||||
}
|
||||
}
|
||||
$peerData['neighbors'] = array_values($neighbors);
|
||||
}
|
||||
|
||||
// Update Pathvector options
|
||||
if (isset($data['pathvector'])) {
|
||||
$peerData['pathvector'] = $this->buildPathvectorOptions(
|
||||
array_merge($peerData['pathvector'], $data['pathvector'])
|
||||
);
|
||||
}
|
||||
|
||||
// Update basic fields
|
||||
$basicFields = [
|
||||
'name', 'description', 'remote_asn', 'node', 'template',
|
||||
'afi', 'templates', 'is_active', 'tags'
|
||||
];
|
||||
|
||||
foreach ($basicFields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$peerData[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
|
||||
$peerData['updated_at'] = date('c');
|
||||
$peers[$id] = $peerData;
|
||||
|
||||
if ($this->db->set('peers', $peers)) {
|
||||
$this->logger->info('peer', "Updated peer: $id");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Peer $id updated successfully",
|
||||
'data' => $peerData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to update peer',
|
||||
'errors' => ['database' => 'Failed to save peer'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete peer
|
||||
*
|
||||
* @param string $id
|
||||
* @return array
|
||||
*/
|
||||
public function delete(string $id): array {
|
||||
$peers = $this->db->get('peers') ?? [];
|
||||
|
||||
if (!isset($peers[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Peer $id not found",
|
||||
];
|
||||
}
|
||||
|
||||
$peerName = $peers[$id]['name'];
|
||||
$peerAsn = $peers[$id]['remote_asn'];
|
||||
unset($peers[$id]);
|
||||
|
||||
if ($this->db->set('peers', $peers)) {
|
||||
$this->logger->warning('peer', "Deleted peer: $id ($peerName - AS$peerAsn)");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Peer $id deleted successfully",
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to delete peer',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build peer data structure
|
||||
*
|
||||
* @param string $id
|
||||
* @param array $data
|
||||
* @param array $neighbors
|
||||
* @return array
|
||||
*/
|
||||
private function buildPeerData(string $id, array $data, array $neighbors): array {
|
||||
return [
|
||||
'id' => $id,
|
||||
'name' => $data['name'] ?? $id,
|
||||
'description' => $data['description'] ?? '',
|
||||
'asn' => (int) $data['asn'],
|
||||
'remote_asn' => (int) $data['remote_asn'],
|
||||
'node' => $data['node'] ?? '',
|
||||
'neighbors' => array_values($neighbors),
|
||||
'afi' => $data['afi'] ?? ['ipv4', 'ipv6'],
|
||||
'template' => $data['template'] ?? '',
|
||||
'templates' => $data['templates'] ?? [],
|
||||
'tags' => $data['tags'] ?? [],
|
||||
'pathvector' => $this->buildPathvectorOptions($data['pathvector'] ?? []),
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Pathvector options structure with ALL supported options
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
private function buildPathvectorOptions(array $data): array {
|
||||
return [
|
||||
// Session control
|
||||
'disabled' => $data['disabled'] ?? false,
|
||||
'import' => $data['import'] ?? true,
|
||||
'export' => $data['export'] ?? true,
|
||||
|
||||
// BGP attributes
|
||||
'local_asn' => $data['local_asn'] ?? null,
|
||||
'prepends' => $data['prepends'] ?? 0,
|
||||
'prepend_path' => $data['prepend_path'] ?? [],
|
||||
'clear_path' => $data['clear_path'] ?? false,
|
||||
'local_pref' => $data['local_pref'] ?? 100,
|
||||
'local_pref4' => $data['local_pref4'] ?? null,
|
||||
'local_pref6' => $data['local_pref6'] ?? null,
|
||||
'set_local_pref' => $data['set_local_pref'] ?? false,
|
||||
'multihop' => $data['multihop'] ?? null,
|
||||
|
||||
// Listening
|
||||
'listen4' => $data['listen4'] ?? '',
|
||||
'listen6' => $data['listen6'] ?? '',
|
||||
'local_port' => $data['local_port'] ?? 179,
|
||||
'neighbor_port' => $data['neighbor_port'] ?? 179,
|
||||
'passive' => $data['passive'] ?? false,
|
||||
'direct' => $data['direct'] ?? false,
|
||||
|
||||
// Next-hop
|
||||
'next_hop_self' => $data['next_hop_self'] ?? false,
|
||||
'next_hop_self_ebgp' => $data['next_hop_self_ebgp'] ?? false,
|
||||
'next_hop_self_ibgp' => $data['next_hop_self_ibgp'] ?? false,
|
||||
'import_next_hop' => $data['import_next_hop'] ?? '',
|
||||
'export_next_hop' => $data['export_next_hop'] ?? '',
|
||||
'enforce_peer_nexthop' => $data['enforce_peer_nexthop'] ?? true,
|
||||
'force_peer_nexthop' => $data['force_peer_nexthop'] ?? false,
|
||||
|
||||
// BFD
|
||||
'bfd' => $data['bfd'] ?? false,
|
||||
|
||||
// Authentication
|
||||
'password' => $data['password'] ?? '',
|
||||
|
||||
// Route server / reflector
|
||||
'rs_client' => $data['rs_client'] ?? false,
|
||||
'rr_client' => $data['rr_client'] ?? false,
|
||||
|
||||
// AS path manipulation
|
||||
'remove_private_asns' => $data['remove_private_asns'] ?? true,
|
||||
'allow_local_as' => $data['allow_local_as'] ?? false,
|
||||
'enforce_first_as' => $data['enforce_first_as'] ?? true,
|
||||
|
||||
// Multi-protocol
|
||||
'mp_unicast_46' => $data['mp_unicast_46'] ?? false,
|
||||
|
||||
// Add-path
|
||||
'add_path_tx' => $data['add_path_tx'] ?? false,
|
||||
'add_path_rx' => $data['add_path_rx'] ?? false,
|
||||
|
||||
// Confederation
|
||||
'confederation' => $data['confederation'] ?? null,
|
||||
'confederation_member' => $data['confederation_member'] ?? false,
|
||||
|
||||
// TTL Security
|
||||
'ttl_security' => $data['ttl_security'] ?? false,
|
||||
|
||||
// Timers and limits
|
||||
'receive_limit4' => $data['receive_limit4'] ?? null,
|
||||
'receive_limit6' => $data['receive_limit6'] ?? null,
|
||||
'receive_limit_violation' => $data['receive_limit_violation'] ?? 'disable',
|
||||
'export_limit4' => $data['export_limit4'] ?? null,
|
||||
'export_limit6' => $data['export_limit6'] ?? null,
|
||||
'export_limit_violation' => $data['export_limit_violation'] ?? 'disable',
|
||||
|
||||
// Session options
|
||||
'interpret_communities' => $data['interpret_communities'] ?? true,
|
||||
'default_local_pref' => $data['default_local_pref'] ?? null,
|
||||
'advertise_hostname' => $data['advertise_hostname'] ?? false,
|
||||
'disable_after_error' => $data['disable_after_error'] ?? false,
|
||||
'prefer_older_routes' => $data['prefer_older_routes'] ?? false,
|
||||
|
||||
// IRR
|
||||
'irr_accept_child_prefixes' => $data['irr_accept_child_prefixes'] ?? false,
|
||||
|
||||
// Communities
|
||||
'add_on_import' => $data['add_on_import'] ?? [],
|
||||
'add_on_export' => $data['add_on_export'] ?? [],
|
||||
'announce' => $data['announce'] ?? [],
|
||||
'remove_communities' => $data['remove_communities'] ?? [],
|
||||
'remove_all_communities' => $data['remove_all_communities'] ?? null,
|
||||
|
||||
// AS preferences
|
||||
'as_prefs' => $data['as_prefs'] ?? [],
|
||||
'community_prefs' => $data['community_prefs'] ?? [],
|
||||
'large_community_prefs' => $data['large_community_prefs'] ?? [],
|
||||
|
||||
// AS-SET
|
||||
'as_set' => $data['as_set'] ?? '',
|
||||
|
||||
// Blackhole
|
||||
'allow_blackhole_community' => $data['allow_blackhole_community'] ?? false,
|
||||
'blackhole_in' => $data['blackhole_in'] ?? false,
|
||||
'blackhole_out' => $data['blackhole_out'] ?? false,
|
||||
|
||||
// Filtering
|
||||
'filter_irr' => $data['filter_irr'] ?? false,
|
||||
'filter_rpki' => $data['filter_rpki'] ?? true,
|
||||
'strict_rpki' => $data['strict_rpki'] ?? false,
|
||||
'filter_max_prefix' => $data['filter_max_prefix'] ?? true,
|
||||
'filter_bogon_routes' => $data['filter_bogon_routes'] ?? true,
|
||||
'filter_bogon_asns' => $data['filter_bogon_asns'] ?? true,
|
||||
'filter_transit_asns' => $data['filter_transit_asns'] ?? false,
|
||||
'filter_prefix_length' => $data['filter_prefix_length'] ?? true,
|
||||
'filter_never_via_route_servers' => $data['filter_never_via_route_servers'] ?? false,
|
||||
'filter_as_set' => $data['filter_as_set'] ?? false,
|
||||
'filter_aspa' => $data['filter_aspa'] ?? false,
|
||||
'filter_blocklist' => $data['filter_blocklist'] ?? true,
|
||||
|
||||
// Transit lock
|
||||
'transit_lock' => $data['transit_lock'] ?? [],
|
||||
|
||||
// Announcement control
|
||||
'dont_announce' => $data['dont_announce'] ?? [],
|
||||
'only_announce' => $data['only_announce'] ?? [],
|
||||
'prefix_communities' => $data['prefix_communities'] ?? [],
|
||||
|
||||
// Auto-config
|
||||
'auto_import_limits' => $data['auto_import_limits'] ?? false,
|
||||
'auto_as_set' => $data['auto_as_set'] ?? false,
|
||||
'auto_as_set_members' => $data['auto_as_set_members'] ?? false,
|
||||
|
||||
// Graceful shutdown
|
||||
'honor_graceful_shutdown' => $data['honor_graceful_shutdown'] ?? true,
|
||||
|
||||
// Prefixes
|
||||
'prefixes' => $data['prefixes'] ?? [],
|
||||
'as_set_members' => $data['as_set_members'] ?? [],
|
||||
|
||||
// BGP Role (RFC 9234)
|
||||
'role' => $data['role'] ?? '',
|
||||
'require_roles' => $data['require_roles'] ?? false,
|
||||
|
||||
// Export options
|
||||
'announce_default' => $data['announce_default'] ?? false,
|
||||
'announce_originated' => $data['announce_originated'] ?? true,
|
||||
'announce_all' => $data['announce_all'] ?? false,
|
||||
|
||||
// Custom config
|
||||
'session_global' => $data['session_global'] ?? '',
|
||||
'pre_import_filter' => $data['pre_import_filter'] ?? '',
|
||||
'post_import_filter' => $data['post_import_filter'] ?? '',
|
||||
'pre_import_accept' => $data['pre_import_accept'] ?? '',
|
||||
'pre_export' => $data['pre_export'] ?? '',
|
||||
'pre_export_final' => $data['pre_export_final'] ?? '',
|
||||
|
||||
// Route optimization
|
||||
'probe_sources' => $data['probe_sources'] ?? [],
|
||||
'optimize_inbound' => $data['optimize_inbound'] ?? false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize peer ID
|
||||
*
|
||||
* @param string $id
|
||||
* @return string
|
||||
*/
|
||||
private function sanitizeId(string $id): string {
|
||||
$id = strtolower(trim($id));
|
||||
$id = preg_replace('/\s+/', '-', $id);
|
||||
$id = preg_replace('/[^a-z0-9-_]/', '', $id);
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count peers
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int {
|
||||
$peers = $this->db->get('peers') ?? [];
|
||||
return count($peers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search peers
|
||||
*
|
||||
* @param string $query
|
||||
* @return array
|
||||
*/
|
||||
public function search(string $query): array {
|
||||
$peers = $this->db->get('peers') ?? [];
|
||||
$query = strtolower($query);
|
||||
|
||||
return array_filter($peers, function($peer) use ($query) {
|
||||
return strpos(strtolower($peer['id']), $query) !== false ||
|
||||
strpos(strtolower($peer['name']), $query) !== false ||
|
||||
strpos((string) $peer['remote_asn'], $query) !== false ||
|
||||
strpos(strtolower($peer['description'] ?? ''), $query) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Export peer to YAML format
|
||||
*
|
||||
* @param string $id
|
||||
* @return string|null
|
||||
*/
|
||||
public function exportYaml(string $id): ?string {
|
||||
$peer = $this->get($id);
|
||||
|
||||
if (!$peer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pv = $peer['pathvector'];
|
||||
$config = [];
|
||||
|
||||
// Required fields
|
||||
$config['asn'] = $peer['remote_asn'];
|
||||
|
||||
if (!empty($peer['template'])) {
|
||||
$config['template'] = $peer['template'];
|
||||
}
|
||||
|
||||
if (!empty($peer['neighbors'])) {
|
||||
$config['neighbors'] = $peer['neighbors'];
|
||||
}
|
||||
|
||||
// Add description if set
|
||||
if (!empty($peer['description'])) {
|
||||
$config['description'] = $peer['description'];
|
||||
}
|
||||
|
||||
// Add all non-default Pathvector options
|
||||
$this->addNonDefaultOptions($config, $pv);
|
||||
|
||||
// Build YAML with peer name as key
|
||||
$peerName = $peer['name'];
|
||||
$yaml = " $peerName:\n";
|
||||
|
||||
foreach ($config as $key => $value) {
|
||||
$yamlKey = str_replace('_', '-', $key);
|
||||
$yaml .= $this->formatYamlEntry($yamlKey, $value, 2);
|
||||
}
|
||||
|
||||
return $yaml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add non-default options to config array
|
||||
*
|
||||
* @param array &$config
|
||||
* @param array $pv
|
||||
*/
|
||||
private function addNonDefaultOptions(array &$config, array $pv): void {
|
||||
$defaults = $this->buildPathvectorOptions([]);
|
||||
|
||||
foreach ($pv as $key => $value) {
|
||||
// Skip if same as default
|
||||
if (isset($defaults[$key]) && $defaults[$key] === $value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty values
|
||||
if ($value === '' || $value === null || $value === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip certain internal fields
|
||||
if (in_array($key, ['template', 'neighbors'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$config[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format YAML entry
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $indent
|
||||
* @return string
|
||||
*/
|
||||
private function formatYamlEntry(string $key, $value, int $indent): string {
|
||||
$prefix = str_repeat(' ', $indent);
|
||||
|
||||
if (is_array($value)) {
|
||||
if (empty($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (array_keys($value) === range(0, count($value) - 1)) {
|
||||
// Sequential array
|
||||
$yaml = "$prefix$key:\n";
|
||||
foreach ($value as $item) {
|
||||
$yaml .= "$prefix - " . $this->formatYamlValue($item) . "\n";
|
||||
}
|
||||
return $yaml;
|
||||
} else {
|
||||
// Associative array
|
||||
$yaml = "$prefix$key:\n";
|
||||
foreach ($value as $k => $v) {
|
||||
$yaml .= $this->formatYamlEntry($k, $v, $indent + 1);
|
||||
}
|
||||
return $yaml;
|
||||
}
|
||||
}
|
||||
|
||||
return "$prefix$key: " . $this->formatYamlValue($value) . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format value for YAML
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
private function formatYamlValue($value): string {
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
if (is_string($value) && (strpos($value, ':') !== false || strpos($value, '#') !== false)) {
|
||||
return '"' . addslashes($value) . '"';
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get peers grouped by type (based on template or ASN type)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGroupedByType(): array {
|
||||
$peers = $this->db->get('peers') ?? [];
|
||||
$grouped = [
|
||||
'upstream' => [],
|
||||
'downstream' => [],
|
||||
'peer' => [],
|
||||
'routeserver' => [],
|
||||
'ibgp' => [],
|
||||
'other' => [],
|
||||
];
|
||||
|
||||
foreach ($peers as $id => $peer) {
|
||||
$template = strtolower($peer['template'] ?? '');
|
||||
|
||||
if (strpos($template, 'upstream') !== false) {
|
||||
$grouped['upstream'][$id] = $peer;
|
||||
} elseif (strpos($template, 'downstream') !== false) {
|
||||
$grouped['downstream'][$id] = $peer;
|
||||
} elseif (strpos($template, 'routeserver') !== false || strpos($template, 'rs') !== false) {
|
||||
$grouped['routeserver'][$id] = $peer;
|
||||
} elseif ($peer['asn'] === $peer['remote_asn']) {
|
||||
$grouped['ibgp'][$id] = $peer;
|
||||
} elseif (strpos($template, 'peer') !== false) {
|
||||
$grouped['peer'][$id] = $peer;
|
||||
} else {
|
||||
$grouped['other'][$id] = $peer;
|
||||
}
|
||||
}
|
||||
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup peers data
|
||||
*
|
||||
* @param string $backupDir
|
||||
* @return string|false
|
||||
*/
|
||||
public function backup(string $backupDir): string|false {
|
||||
return $this->db->backup($backupDir);
|
||||
}
|
||||
}
|
||||
562
pathvector-admin/lib/Template.php
Normal file
562
pathvector-admin/lib/Template.php
Normal file
@@ -0,0 +1,562 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Class
|
||||
*
|
||||
* Manages reusable templates for ASNs, nodes, and peers
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/FlatFileDB.php';
|
||||
require_once __DIR__ . '/Logger.php';
|
||||
require_once __DIR__ . '/Validator.php';
|
||||
|
||||
class Template {
|
||||
private FlatFileDB $db;
|
||||
private Logger $logger;
|
||||
private Validator $validator;
|
||||
|
||||
// Template types
|
||||
public const TYPE_PEER = 'peer';
|
||||
public const TYPE_ASN = 'asn';
|
||||
public const TYPE_NODE = 'node';
|
||||
public const TYPE_POLICY = 'policy';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $dataFile Path to templates JSON file
|
||||
* @param Logger $logger Logger instance
|
||||
*/
|
||||
public function __construct(string $dataFile, Logger $logger) {
|
||||
$this->db = new FlatFileDB($dataFile);
|
||||
$this->logger = $logger;
|
||||
$this->validator = new Validator();
|
||||
|
||||
if (!$this->db->exists('templates')) {
|
||||
$this->db->set('templates', []);
|
||||
$this->initializeDefaultTemplates();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default peer templates
|
||||
*/
|
||||
private function initializeDefaultTemplates(): void {
|
||||
$defaults = [
|
||||
'upstream' => [
|
||||
'id' => 'upstream',
|
||||
'name' => 'Upstream Provider',
|
||||
'type' => self::TYPE_PEER,
|
||||
'description' => 'Template for upstream transit providers',
|
||||
'pathvector' => [
|
||||
'allow_local_as' => true,
|
||||
'local_pref' => 80,
|
||||
'remove_all_communities' => null,
|
||||
'filter_rpki' => true,
|
||||
'filter_bogon_routes' => true,
|
||||
'filter_bogon_asns' => true,
|
||||
],
|
||||
'is_builtin' => true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
],
|
||||
'downstream' => [
|
||||
'id' => 'downstream',
|
||||
'name' => 'Downstream Customer',
|
||||
'type' => self::TYPE_PEER,
|
||||
'description' => 'Template for downstream customers',
|
||||
'pathvector' => [
|
||||
'filter_irr' => true,
|
||||
'filter_rpki' => true,
|
||||
'filter_transit_asns' => true,
|
||||
'auto_import_limits' => true,
|
||||
'auto_as_set' => true,
|
||||
'allow_blackhole_community' => true,
|
||||
'announce_default' => true,
|
||||
'local_pref' => 200,
|
||||
],
|
||||
'is_builtin' => true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
],
|
||||
'peer' => [
|
||||
'id' => 'peer',
|
||||
'name' => 'Peering Partner',
|
||||
'type' => self::TYPE_PEER,
|
||||
'description' => 'Template for bilateral peering sessions',
|
||||
'pathvector' => [
|
||||
'filter_irr' => true,
|
||||
'filter_rpki' => true,
|
||||
'filter_transit_asns' => true,
|
||||
'auto_import_limits' => true,
|
||||
'auto_as_set' => true,
|
||||
'local_pref' => 100,
|
||||
],
|
||||
'is_builtin' => true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
],
|
||||
'routeserver' => [
|
||||
'id' => 'routeserver',
|
||||
'name' => 'Route Server',
|
||||
'type' => self::TYPE_PEER,
|
||||
'description' => 'Template for IXP route servers',
|
||||
'pathvector' => [
|
||||
'filter_transit_asns' => true,
|
||||
'auto_import_limits' => true,
|
||||
'enforce_peer_nexthop' => false,
|
||||
'enforce_first_as' => false,
|
||||
'local_pref' => 90,
|
||||
],
|
||||
'is_builtin' => true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
],
|
||||
'ibgp' => [
|
||||
'id' => 'ibgp',
|
||||
'name' => 'Internal BGP',
|
||||
'type' => self::TYPE_PEER,
|
||||
'description' => 'Template for iBGP sessions within the same AS',
|
||||
'pathvector' => [
|
||||
'next_hop_self_ibgp' => true,
|
||||
'filter_rpki' => false,
|
||||
'filter_bogon_routes' => false,
|
||||
'filter_bogon_asns' => false,
|
||||
'remove_private_asns' => false,
|
||||
],
|
||||
'is_builtin' => true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
],
|
||||
];
|
||||
|
||||
$this->db->set('templates', $defaults);
|
||||
$this->logger->info('template', 'Initialized default templates');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all templates
|
||||
*
|
||||
* @param string|null $type Filter by type
|
||||
* @return array
|
||||
*/
|
||||
public function getAll(?string $type = null): array {
|
||||
$templates = $this->db->get('templates') ?? [];
|
||||
|
||||
if ($type !== null) {
|
||||
return array_filter($templates, fn($t) => ($t['type'] ?? '') === $type);
|
||||
}
|
||||
|
||||
return $templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template by ID
|
||||
*
|
||||
* @param string $id
|
||||
* @return array|null
|
||||
*/
|
||||
public function get(string $id): ?array {
|
||||
$templates = $this->db->get('templates') ?? [];
|
||||
return $templates[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new template
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function create(array $data): array {
|
||||
if (empty($data['id'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Template ID is required',
|
||||
'errors' => ['id' => 'Template ID is required'],
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($data['type']) || !in_array($data['type'], [self::TYPE_PEER, self::TYPE_ASN, self::TYPE_NODE, self::TYPE_POLICY])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Valid template type is required',
|
||||
'errors' => ['type' => 'Template type must be: peer, asn, node, or policy'],
|
||||
];
|
||||
}
|
||||
|
||||
$templates = $this->db->get('templates') ?? [];
|
||||
$id = $this->sanitizeId($data['id']);
|
||||
|
||||
if (isset($templates[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Template $id already exists",
|
||||
'errors' => ['id' => 'Template ID already exists'],
|
||||
];
|
||||
}
|
||||
|
||||
$templateData = [
|
||||
'id' => $id,
|
||||
'name' => $data['name'] ?? $id,
|
||||
'type' => $data['type'],
|
||||
'description' => $data['description'] ?? '',
|
||||
'parent' => $data['parent'] ?? null, // For inheritance
|
||||
'pathvector' => $data['pathvector'] ?? [],
|
||||
'variables' => $data['variables'] ?? [], // Variable substitution
|
||||
'is_builtin' => false,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
];
|
||||
|
||||
$templates[$id] = $templateData;
|
||||
|
||||
if ($this->db->set('templates', $templates)) {
|
||||
$this->logger->success('template', "Created template: $id ({$templateData['type']})");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Template $id created successfully",
|
||||
'data' => $templateData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to save template',
|
||||
'errors' => ['database' => 'Failed to save template'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update template
|
||||
*
|
||||
* @param string $id
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function update(string $id, array $data): array {
|
||||
$templates = $this->db->get('templates') ?? [];
|
||||
|
||||
if (!isset($templates[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Template $id not found",
|
||||
'errors' => ['id' => 'Template not found'],
|
||||
];
|
||||
}
|
||||
|
||||
// Don't allow modifying builtin templates (only pathvector options)
|
||||
if ($templates[$id]['is_builtin'] ?? false) {
|
||||
$templateData = $templates[$id];
|
||||
|
||||
if (isset($data['pathvector'])) {
|
||||
$templateData['pathvector'] = array_merge(
|
||||
$templateData['pathvector'] ?? [],
|
||||
$data['pathvector']
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($data['description'])) {
|
||||
$templateData['description'] = $data['description'];
|
||||
}
|
||||
} else {
|
||||
$templateData = $templates[$id];
|
||||
|
||||
$allowedFields = ['name', 'description', 'parent', 'pathvector', 'variables'];
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
if ($field === 'pathvector') {
|
||||
$templateData[$field] = array_merge(
|
||||
$templateData[$field] ?? [],
|
||||
$data[$field]
|
||||
);
|
||||
} else {
|
||||
$templateData[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$templateData['updated_at'] = date('c');
|
||||
$templates[$id] = $templateData;
|
||||
|
||||
if ($this->db->set('templates', $templates)) {
|
||||
$this->logger->info('template', "Updated template: $id");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Template $id updated successfully",
|
||||
'data' => $templateData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to update template',
|
||||
'errors' => ['database' => 'Failed to save template'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete template
|
||||
*
|
||||
* @param string $id
|
||||
* @return array
|
||||
*/
|
||||
public function delete(string $id): array {
|
||||
$templates = $this->db->get('templates') ?? [];
|
||||
|
||||
if (!isset($templates[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Template $id not found",
|
||||
];
|
||||
}
|
||||
|
||||
if ($templates[$id]['is_builtin'] ?? false) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Cannot delete builtin templates',
|
||||
];
|
||||
}
|
||||
|
||||
$templateName = $templates[$id]['name'];
|
||||
unset($templates[$id]);
|
||||
|
||||
if ($this->db->set('templates', $templates)) {
|
||||
$this->logger->warning('template', "Deleted template: $id ($templateName)");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Template $id deleted successfully",
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to delete template',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve template with inheritance
|
||||
*
|
||||
* @param string $id
|
||||
* @return array
|
||||
*/
|
||||
public function resolve(string $id): array {
|
||||
$template = $this->get($id);
|
||||
|
||||
if (!$template) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$resolved = $template['pathvector'] ?? [];
|
||||
|
||||
// Apply parent template if exists
|
||||
if (!empty($template['parent'])) {
|
||||
$parentResolved = $this->resolve($template['parent']);
|
||||
$resolved = array_merge($parentResolved, $resolved);
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply template to peer data with variable substitution
|
||||
*
|
||||
* @param array $peerData
|
||||
* @param string $templateId
|
||||
* @param array $variables
|
||||
* @return array
|
||||
*/
|
||||
public function applyToPeer(array $peerData, string $templateId, array $variables = []): array {
|
||||
$resolved = $this->resolve($templateId);
|
||||
|
||||
if (empty($resolved)) {
|
||||
return $peerData;
|
||||
}
|
||||
|
||||
// Apply variable substitution
|
||||
$resolved = $this->substituteVariables($resolved, $variables);
|
||||
|
||||
// Merge template with peer data (peer data takes precedence)
|
||||
$peerData['pathvector'] = array_merge(
|
||||
$resolved,
|
||||
$peerData['pathvector'] ?? []
|
||||
);
|
||||
|
||||
return $peerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute variables in template
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $variables
|
||||
* @return array
|
||||
*/
|
||||
private function substituteVariables(array $data, array $variables): array {
|
||||
array_walk_recursive($data, function(&$value) use ($variables) {
|
||||
if (is_string($value)) {
|
||||
foreach ($variables as $key => $replacement) {
|
||||
$value = str_replace('{{' . $key . '}}', $replacement, $value);
|
||||
$value = str_replace('<pathvector.' . $key . '>', $replacement, $value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview template as YAML
|
||||
*
|
||||
* @param string $id
|
||||
* @return string|null
|
||||
*/
|
||||
public function previewYaml(string $id): ?string {
|
||||
$template = $this->get($id);
|
||||
|
||||
if (!$template || $template['type'] !== self::TYPE_PEER) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$resolved = $this->resolve($id);
|
||||
|
||||
$yaml = "# Template: {$template['name']}\n";
|
||||
$yaml .= "# Type: {$template['type']}\n";
|
||||
|
||||
if (!empty($template['description'])) {
|
||||
$yaml .= "# {$template['description']}\n";
|
||||
}
|
||||
|
||||
$yaml .= "\ntemplates:\n";
|
||||
$yaml .= " {$template['id']}:\n";
|
||||
|
||||
foreach ($resolved as $key => $value) {
|
||||
if ($value === null || $value === '' || $value === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$yamlKey = str_replace('_', '-', $key);
|
||||
$yaml .= $this->formatYamlEntry($yamlKey, $value, 2);
|
||||
}
|
||||
|
||||
return $yaml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format YAML entry
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $indent
|
||||
* @return string
|
||||
*/
|
||||
private function formatYamlEntry(string $key, $value, int $indent): string {
|
||||
$prefix = str_repeat(' ', $indent);
|
||||
|
||||
if (is_array($value)) {
|
||||
if (empty($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (array_keys($value) === range(0, count($value) - 1)) {
|
||||
$yaml = "$prefix$key:\n";
|
||||
foreach ($value as $item) {
|
||||
$yaml .= "$prefix - " . $this->formatYamlValue($item) . "\n";
|
||||
}
|
||||
return $yaml;
|
||||
} else {
|
||||
$yaml = "$prefix$key:\n";
|
||||
foreach ($value as $k => $v) {
|
||||
$yaml .= $this->formatYamlEntry($k, $v, $indent + 1);
|
||||
}
|
||||
return $yaml;
|
||||
}
|
||||
}
|
||||
|
||||
return "$prefix$key: " . $this->formatYamlValue($value) . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format value for YAML
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
private function formatYamlValue($value): string {
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
if (is_string($value) && (strpos($value, ':') !== false || strpos($value, '#') !== false)) {
|
||||
return '"' . addslashes($value) . '"';
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize template ID
|
||||
*
|
||||
* @param string $id
|
||||
* @return string
|
||||
*/
|
||||
private function sanitizeId(string $id): string {
|
||||
$id = strtolower(trim($id));
|
||||
$id = preg_replace('/\s+/', '-', $id);
|
||||
$id = preg_replace('/[^a-z0-9-_]/', '', $id);
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count templates
|
||||
*
|
||||
* @param string|null $type
|
||||
* @return int
|
||||
*/
|
||||
public function count(?string $type = null): int {
|
||||
return count($this->getAll($type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone template
|
||||
*
|
||||
* @param string $sourceId
|
||||
* @param string $newId
|
||||
* @param string $newName
|
||||
* @return array
|
||||
*/
|
||||
public function cloneTemplate(string $sourceId, string $newId, string $newName = ''): array {
|
||||
$source = $this->get($sourceId);
|
||||
|
||||
if (!$source) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Source template $sourceId not found",
|
||||
];
|
||||
}
|
||||
|
||||
$data = $source;
|
||||
$data['id'] = $newId;
|
||||
$data['name'] = $newName ?: $source['name'] . ' (Copy)';
|
||||
$data['is_builtin'] = false;
|
||||
$data['parent'] = $sourceId; // Inherit from source
|
||||
unset($data['created_at'], $data['updated_at']);
|
||||
|
||||
return $this->create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup templates data
|
||||
*
|
||||
* @param string $backupDir
|
||||
* @return string|false
|
||||
*/
|
||||
public function backup(string $backupDir): string|false {
|
||||
return $this->db->backup($backupDir);
|
||||
}
|
||||
}
|
||||
638
pathvector-admin/lib/Validator.php
Normal file
638
pathvector-admin/lib/Validator.php
Normal file
@@ -0,0 +1,638 @@
|
||||
<?php
|
||||
/**
|
||||
* Validator Class
|
||||
*
|
||||
* Validates Pathvector configurations and ensures correctness before generating BIRD 3 config
|
||||
*/
|
||||
|
||||
class Validator {
|
||||
private array $errors = [];
|
||||
private array $warnings = [];
|
||||
|
||||
// Valid ASN ranges
|
||||
private const ASN_MIN = 1;
|
||||
private const ASN_MAX = 4294967295;
|
||||
private const PRIVATE_ASN_START_16 = 64512;
|
||||
private const PRIVATE_ASN_END_16 = 65534;
|
||||
private const PRIVATE_ASN_START_32 = 4200000000;
|
||||
private const PRIVATE_ASN_END_32 = 4294967294;
|
||||
|
||||
// Valid BGP roles (RFC 9234)
|
||||
private const VALID_BGP_ROLES = ['provider', 'rs-server', 'rs-client', 'customer', 'peer'];
|
||||
|
||||
// Valid limit violation actions
|
||||
private const VALID_LIMIT_ACTIONS = ['disable', 'restart', 'block', 'warn'];
|
||||
|
||||
// Transit-free ASNs (tier-1 networks)
|
||||
private const TRANSIT_FREE_ASNS = [
|
||||
174, 209, 286, 701, 1239, 1299, 2828, 2914, 3257, 3320, 3356, 3491,
|
||||
5511, 6453, 6461, 6762, 6830, 7018, 12956
|
||||
];
|
||||
|
||||
/**
|
||||
* Get validation errors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getErrors(): array {
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation warnings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getWarnings(): array {
|
||||
return $this->warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if validation passed
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(): bool {
|
||||
return empty($this->errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear errors and warnings
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clear(): void {
|
||||
$this->errors = [];
|
||||
$this->warnings = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add error
|
||||
*
|
||||
* @param string $field
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
private function addError(string $field, string $message): void {
|
||||
$this->errors[$field] = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add warning
|
||||
*
|
||||
* @param string $field
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
private function addWarning(string $field, string $message): void {
|
||||
$this->warnings[$field] = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate ASN
|
||||
*
|
||||
* @param mixed $asn
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateAsn($asn, string $field = 'asn'): bool {
|
||||
if (!is_numeric($asn)) {
|
||||
$this->addError($field, 'ASN must be a number');
|
||||
return false;
|
||||
}
|
||||
|
||||
$asn = (int) $asn;
|
||||
|
||||
if ($asn < self::ASN_MIN || $asn > self::ASN_MAX) {
|
||||
$this->addError($field, "ASN must be between " . self::ASN_MIN . " and " . self::ASN_MAX);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Warn about reserved/private ASNs
|
||||
if ($asn === 0) {
|
||||
$this->addError($field, 'ASN 0 is reserved');
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($asn === 23456) {
|
||||
$this->addError($field, 'ASN 23456 is reserved for AS_TRANS (RFC 6793)');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($asn >= self::PRIVATE_ASN_START_16 && $asn <= self::PRIVATE_ASN_END_16) ||
|
||||
($asn >= self::PRIVATE_ASN_START_32 && $asn <= self::PRIVATE_ASN_END_32)) {
|
||||
$this->addWarning($field, 'This is a private ASN');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IPv4 address
|
||||
*
|
||||
* @param string $ip
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateIPv4(string $ip, string $field = 'ip'): bool {
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$this->addError($field, 'Invalid IPv4 address');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IPv6 address
|
||||
*
|
||||
* @param string $ip
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateIPv6(string $ip, string $field = 'ip'): bool {
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$this->addError($field, 'Invalid IPv6 address');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IP address (IPv4 or IPv6)
|
||||
*
|
||||
* @param string $ip
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateIP(string $ip, string $field = 'ip'): bool {
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
$this->addError($field, 'Invalid IP address');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IPv4 prefix
|
||||
*
|
||||
* @param string $prefix
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateIPv4Prefix(string $prefix, string $field = 'prefix'): bool {
|
||||
$parts = explode('/', $prefix);
|
||||
|
||||
if (count($parts) !== 2) {
|
||||
$this->addError($field, 'Invalid prefix format (expected x.x.x.x/xx)');
|
||||
return false;
|
||||
}
|
||||
|
||||
[$ip, $length] = $parts;
|
||||
|
||||
if (!$this->validateIPv4($ip, $field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$length = (int) $length;
|
||||
if ($length < 0 || $length > 32) {
|
||||
$this->addError($field, 'Prefix length must be between 0 and 32');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IPv6 prefix
|
||||
*
|
||||
* @param string $prefix
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateIPv6Prefix(string $prefix, string $field = 'prefix'): bool {
|
||||
$parts = explode('/', $prefix);
|
||||
|
||||
if (count($parts) !== 2) {
|
||||
$this->addError($field, 'Invalid prefix format (expected xxxx::/xx)');
|
||||
return false;
|
||||
}
|
||||
|
||||
[$ip, $length] = $parts;
|
||||
|
||||
if (!$this->validateIPv6($ip, $field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$length = (int) $length;
|
||||
if ($length < 0 || $length > 128) {
|
||||
$this->addError($field, 'Prefix length must be between 0 and 128');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate prefix (IPv4 or IPv6)
|
||||
*
|
||||
* @param string $prefix
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validatePrefix(string $prefix, string $field = 'prefix'): bool {
|
||||
if (strpos($prefix, ':') !== false) {
|
||||
return $this->validateIPv6Prefix($prefix, $field);
|
||||
}
|
||||
|
||||
return $this->validateIPv4Prefix($prefix, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate router ID
|
||||
*
|
||||
* @param string $routerId
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateRouterId(string $routerId, string $field = 'router_id'): bool {
|
||||
return $this->validateIPv4($routerId, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate BGP community (standard)
|
||||
*
|
||||
* @param string $community
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateStandardCommunity(string $community, string $field = 'community'): bool {
|
||||
// Format: ASN,value or ASN:value
|
||||
$pattern = '/^(\d+)[,:](\d+)$/';
|
||||
|
||||
if (!preg_match($pattern, $community, $matches)) {
|
||||
$this->addError($field, 'Invalid standard community format (expected ASN,value or ASN:value)');
|
||||
return false;
|
||||
}
|
||||
|
||||
$asn = (int) $matches[1];
|
||||
$value = (int) $matches[2];
|
||||
|
||||
if ($asn > 65535 || $value > 65535) {
|
||||
$this->addError($field, 'Standard community values must be <= 65535');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate BGP large community
|
||||
*
|
||||
* @param string $community
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateLargeCommunity(string $community, string $field = 'community'): bool {
|
||||
// Format: ASN:value1:value2
|
||||
$pattern = '/^(\d+):(\d+):(\d+)$/';
|
||||
|
||||
if (!preg_match($pattern, $community, $matches)) {
|
||||
$this->addError($field, 'Invalid large community format (expected ASN:value1:value2)');
|
||||
return false;
|
||||
}
|
||||
|
||||
$asn = (int) $matches[1];
|
||||
$val1 = (int) $matches[2];
|
||||
$val2 = (int) $matches[3];
|
||||
|
||||
if ($asn > self::ASN_MAX || $val1 > self::ASN_MAX || $val2 > self::ASN_MAX) {
|
||||
$this->addError($field, 'Large community values must be <= 4294967295');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate community (standard or large)
|
||||
*
|
||||
* @param string $community
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateCommunity(string $community, string $field = 'community'): bool {
|
||||
// Try large community first (has 3 parts)
|
||||
if (preg_match('/^\d+:\d+:\d+$/', $community)) {
|
||||
return $this->validateLargeCommunity($community, $field);
|
||||
}
|
||||
|
||||
return $this->validateStandardCommunity($community, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate BGP role (RFC 9234)
|
||||
*
|
||||
* @param string $role
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateBgpRole(string $role, string $field = 'role'): bool {
|
||||
$role = strtolower(str_replace('_', '-', $role));
|
||||
|
||||
if (!in_array($role, self::VALID_BGP_ROLES)) {
|
||||
$this->addError($field, 'Invalid BGP role. Must be one of: ' . implode(', ', self::VALID_BGP_ROLES));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate TCP port
|
||||
*
|
||||
* @param mixed $port
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validatePort(mixed $port, string $field = 'port'): bool {
|
||||
if (!is_numeric($port)) {
|
||||
$this->addError($field, 'Port must be a number');
|
||||
return false;
|
||||
}
|
||||
|
||||
$port = (int) $port;
|
||||
|
||||
if ($port < 1 || $port > 65535) {
|
||||
$this->addError($field, 'Port must be between 1 and 65535');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate limit violation action
|
||||
*
|
||||
* @param string $action
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateLimitAction(string $action, string $field = 'action'): bool {
|
||||
if (!in_array($action, self::VALID_LIMIT_ACTIONS)) {
|
||||
$this->addError($field, 'Invalid limit action. Must be one of: ' . implode(', ', self::VALID_LIMIT_ACTIONS));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate hostname
|
||||
*
|
||||
* @param string $hostname
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateHostname(string $hostname, string $field = 'hostname'): bool {
|
||||
$pattern = '/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/';
|
||||
|
||||
if (!preg_match($pattern, $hostname)) {
|
||||
$this->addError($field, 'Invalid hostname format');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($hostname) > 253) {
|
||||
$this->addError($field, 'Hostname must not exceed 253 characters');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate AS-SET name
|
||||
*
|
||||
* @param string $asSet
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function validateAsSet(string $asSet, string $field = 'as_set'): bool {
|
||||
// AS-SET names typically start with AS and can contain alphanumeric chars, hyphens, and colons
|
||||
$pattern = '/^(AS-?)?[A-Z0-9][A-Z0-9-:]*$/i';
|
||||
|
||||
if (!preg_match($pattern, $asSet)) {
|
||||
$this->addError($field, 'Invalid AS-SET name format');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate peer configuration
|
||||
*
|
||||
* @param array $peer
|
||||
* @return bool
|
||||
*/
|
||||
public function validatePeerConfig(array $peer): bool {
|
||||
$this->clear();
|
||||
|
||||
// Required fields
|
||||
if (empty($peer['asn'])) {
|
||||
$this->addError('asn', 'ASN is required');
|
||||
} else {
|
||||
$this->validateAsn($peer['asn'], 'asn');
|
||||
}
|
||||
|
||||
if (empty($peer['neighbors']) || !is_array($peer['neighbors']) || count($peer['neighbors']) === 0) {
|
||||
$this->addError('neighbors', 'At least one neighbor IP is required');
|
||||
} else {
|
||||
foreach ($peer['neighbors'] as $i => $neighbor) {
|
||||
$this->validateIP($neighbor, "neighbors[$i]");
|
||||
}
|
||||
}
|
||||
|
||||
// Optional fields validation
|
||||
if (!empty($peer['remote_asn'])) {
|
||||
$this->validateAsn($peer['remote_asn'], 'remote_asn');
|
||||
}
|
||||
|
||||
if (!empty($peer['local_asn'])) {
|
||||
$this->validateAsn($peer['local_asn'], 'local_asn');
|
||||
}
|
||||
|
||||
if (!empty($peer['local_port'])) {
|
||||
$this->validatePort($peer['local_port'], 'local_port');
|
||||
}
|
||||
|
||||
if (!empty($peer['neighbor_port'])) {
|
||||
$this->validatePort($peer['neighbor_port'], 'neighbor_port');
|
||||
}
|
||||
|
||||
if (!empty($peer['role'])) {
|
||||
$this->validateBgpRole($peer['role'], 'role');
|
||||
}
|
||||
|
||||
if (!empty($peer['router_id'])) {
|
||||
$this->validateRouterId($peer['router_id'], 'router_id');
|
||||
}
|
||||
|
||||
if (!empty($peer['receive_limit_violation'])) {
|
||||
$this->validateLimitAction($peer['receive_limit_violation'], 'receive_limit_violation');
|
||||
}
|
||||
|
||||
if (!empty($peer['export_limit_violation'])) {
|
||||
$this->validateLimitAction($peer['export_limit_violation'], 'export_limit_violation');
|
||||
}
|
||||
|
||||
// Validate communities
|
||||
if (!empty($peer['add_on_import']) && is_array($peer['add_on_import'])) {
|
||||
foreach ($peer['add_on_import'] as $i => $community) {
|
||||
$this->validateCommunity($community, "add_on_import[$i]");
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($peer['add_on_export']) && is_array($peer['add_on_export'])) {
|
||||
foreach ($peer['add_on_export'] as $i => $community) {
|
||||
$this->validateCommunity($community, "add_on_export[$i]");
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($peer['announce']) && is_array($peer['announce'])) {
|
||||
foreach ($peer['announce'] as $i => $community) {
|
||||
$this->validateCommunity($community, "announce[$i]");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate prefixes
|
||||
if (!empty($peer['prefixes']) && is_array($peer['prefixes'])) {
|
||||
foreach ($peer['prefixes'] as $i => $prefix) {
|
||||
$this->validatePrefix($prefix, "prefixes[$i]");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate as-set
|
||||
if (!empty($peer['as_set'])) {
|
||||
$this->validateAsSet($peer['as_set'], 'as_set');
|
||||
}
|
||||
|
||||
// Logical validation
|
||||
if (!empty($peer['only_announce']) && !empty($peer['announce_all']) && $peer['announce_all']) {
|
||||
$this->addError('announce_all', 'only-announce and announce-all cannot both be true');
|
||||
}
|
||||
|
||||
if (!empty($peer['default_local_pref']) && !empty($peer['optimize_inbound']) && $peer['optimize_inbound']) {
|
||||
$this->addError('optimize_inbound', 'default-local-pref and optimize-inbound cannot both be set');
|
||||
}
|
||||
|
||||
return $this->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate global config
|
||||
*
|
||||
* @param array $config
|
||||
* @return bool
|
||||
*/
|
||||
public function validateGlobalConfig(array $config): bool {
|
||||
$this->clear();
|
||||
|
||||
// Required fields
|
||||
if (empty($config['asn'])) {
|
||||
$this->addError('asn', 'ASN is required');
|
||||
} else {
|
||||
$this->validateAsn($config['asn'], 'asn');
|
||||
}
|
||||
|
||||
if (empty($config['router_id'])) {
|
||||
$this->addError('router_id', 'Router ID is required');
|
||||
} else {
|
||||
$this->validateRouterId($config['router_id'], 'router_id');
|
||||
}
|
||||
|
||||
// Optional fields
|
||||
if (!empty($config['source4'])) {
|
||||
$this->validateIPv4($config['source4'], 'source4');
|
||||
}
|
||||
|
||||
if (!empty($config['source6'])) {
|
||||
$this->validateIPv6($config['source6'], 'source6');
|
||||
}
|
||||
|
||||
// Validate prefixes
|
||||
if (!empty($config['prefixes']) && is_array($config['prefixes'])) {
|
||||
foreach ($config['prefixes'] as $i => $prefix) {
|
||||
$this->validatePrefix($prefix, "prefixes[$i]");
|
||||
}
|
||||
}
|
||||
|
||||
return $this->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate node configuration
|
||||
*
|
||||
* @param array $node
|
||||
* @return bool
|
||||
*/
|
||||
public function validateNodeConfig(array $node): bool {
|
||||
$this->clear();
|
||||
|
||||
if (empty($node['asn'])) {
|
||||
$this->addError('asn', 'ASN is required');
|
||||
} else {
|
||||
$this->validateAsn($node['asn'], 'asn');
|
||||
}
|
||||
|
||||
if (!empty($node['router_id'])) {
|
||||
$this->validateRouterId($node['router_id'], 'router_id');
|
||||
}
|
||||
|
||||
if (!empty($node['hostname'])) {
|
||||
$this->validateHostname($node['hostname'], 'hostname');
|
||||
}
|
||||
|
||||
return $this->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ASN is a transit-free network
|
||||
*
|
||||
* @param int $asn
|
||||
* @return bool
|
||||
*/
|
||||
public function isTransitFreeAsn(int $asn): bool {
|
||||
return in_array($asn, self::TRANSIT_FREE_ASNS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ASN is private
|
||||
*
|
||||
* @param int $asn
|
||||
* @return bool
|
||||
*/
|
||||
public function isPrivateAsn(int $asn): bool {
|
||||
return ($asn >= self::PRIVATE_ASN_START_16 && $asn <= self::PRIVATE_ASN_END_16) ||
|
||||
($asn >= self::PRIVATE_ASN_START_32 && $asn <= self::PRIVATE_ASN_END_32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required fields
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $requiredFields
|
||||
* @return bool
|
||||
*/
|
||||
public function validateRequired(array $data, array $requiredFields): bool {
|
||||
foreach ($requiredFields as $field) {
|
||||
if (!isset($data[$field]) || $data[$field] === '' || $data[$field] === null) {
|
||||
$this->addError($field, ucfirst(str_replace('_', ' ', $field)) . ' is required');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->isValid();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user