127 lines
3.9 KiB
PHP
127 lines
3.9 KiB
PHP
<?php
|
|
/**
|
|
* VC Gateway Helpers
|
|
*
|
|
* Shared functions for the VC Gateway module.
|
|
* Handles visitor authentication, rate limiting, and invite link validation.
|
|
*/
|
|
|
|
require_once __DIR__ . '/../../helpers.php';
|
|
|
|
// Rate limit: max messages per window
|
|
define('VCGW_RATE_LIMIT_MAX', 10); // 10 messages
|
|
define('VCGW_RATE_LIMIT_WINDOW_SEC', 300); // per 5-minute window
|
|
|
|
/**
|
|
* Authenticate a visitor via X-Visitor-Token header.
|
|
* Returns the visitor row (with invite link data) or aborts with 401.
|
|
*/
|
|
function requireVisitorAuth(): array {
|
|
$token = headerValue('X-Visitor-Token');
|
|
if (empty($token)) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'visitor_token_required'], 401);
|
|
}
|
|
|
|
$visitor = queryOne(
|
|
"SELECT v.*, il.Token AS InviteToken, il.AllowedChannels, il.HostAddress,
|
|
il.IsRevoked AS InviteRevoked, il.ExpiresAt AS InviteExpiresAt
|
|
FROM Hub_Visitors v
|
|
JOIN Hub_InviteLinks il ON il.ID = v.InviteLinkID
|
|
WHERE v.VisitorToken = ?
|
|
LIMIT 1",
|
|
[$token]
|
|
);
|
|
|
|
if (!$visitor) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'invalid_visitor_token'], 401);
|
|
}
|
|
|
|
// Check if the underlying invite link is still valid
|
|
if ($visitor['InviteRevoked']) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'invite_revoked'], 403);
|
|
}
|
|
|
|
if ($visitor['InviteExpiresAt'] && strtotime($visitor['InviteExpiresAt']) < time()) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'invite_expired'], 403);
|
|
}
|
|
|
|
// Update last active
|
|
queryTimed("UPDATE Hub_Visitors SET LastActiveAt = NOW() WHERE ID = ?", [$visitor['ID']]);
|
|
|
|
return $visitor;
|
|
}
|
|
|
|
/**
|
|
* Get the allowed channel IDs for a visitor.
|
|
*/
|
|
function getVisitorAllowedChannels(array $visitor): array {
|
|
$channels = json_decode($visitor['AllowedChannels'], true);
|
|
return is_array($channels) ? array_map('intval', $channels) : [];
|
|
}
|
|
|
|
/**
|
|
* Check if a visitor can read a specific channel.
|
|
*/
|
|
function visitorCanReadChannel(array $visitor, int $channelId): bool {
|
|
$allowed = getVisitorAllowedChannels($visitor);
|
|
return in_array($channelId, $allowed, true);
|
|
}
|
|
|
|
/**
|
|
* Check rate limit for a visitor. Aborts with 429 if exceeded.
|
|
*/
|
|
function checkVisitorRateLimit(int $visitorId): void {
|
|
// Current window start (rounded to 5-min intervals)
|
|
$windowSec = VCGW_RATE_LIMIT_WINDOW_SEC;
|
|
$windowStart = date('Y-m-d H:i:s', (int)(floor(time() / $windowSec) * $windowSec));
|
|
|
|
$row = queryOne(
|
|
"SELECT MessageCount FROM Hub_VisitorRateLimit
|
|
WHERE VisitorID = ? AND WindowStart = ?",
|
|
[$visitorId, $windowStart]
|
|
);
|
|
|
|
$currentCount = $row ? (int)$row['MessageCount'] : 0;
|
|
|
|
if ($currentCount >= VCGW_RATE_LIMIT_MAX) {
|
|
$resetAt = (int)(floor(time() / $windowSec) * $windowSec) + $windowSec;
|
|
jsonResponse([
|
|
'OK' => false,
|
|
'ERROR' => 'rate_limit_exceeded',
|
|
'Limit' => VCGW_RATE_LIMIT_MAX,
|
|
'WindowSeconds' => $windowSec,
|
|
'ResetsAt' => toISO8601(date('Y-m-d H:i:s', $resetAt)),
|
|
], 429);
|
|
}
|
|
|
|
// Upsert the count
|
|
queryTimed(
|
|
"INSERT INTO Hub_VisitorRateLimit (VisitorID, WindowStart, MessageCount)
|
|
VALUES (?, ?, 1)
|
|
ON DUPLICATE KEY UPDATE MessageCount = MessageCount + 1",
|
|
[$visitorId, $windowStart]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Require that the caller is a Sprinter agent (via X-Agent-Address header).
|
|
* Returns the agent address string.
|
|
*/
|
|
function requireAgentAuth(): string {
|
|
$address = headerValue('X-Agent-Address');
|
|
if (empty($address)) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'agent_address_required'], 401);
|
|
}
|
|
|
|
// Verify the agent exists in Sprinter_Agents
|
|
$agent = queryOne(
|
|
"SELECT ID, FullAddress FROM Sprinter_Agents WHERE FullAddress = ? AND IsActive = 1 LIMIT 1",
|
|
[$address]
|
|
);
|
|
|
|
if (!$agent) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'agent_not_found'], 401);
|
|
}
|
|
|
|
return $agent['FullAddress'];
|
|
}
|