RequestProfile.php 2.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. <?php
  2. declare(strict_types=1);
  3. namespace app\miniapi\protection;
  4. use Webman\Http\Request;
  5. use app\miniapi\http\ClientIp;
  6. use app\miniapi\http\Pagination;
  7. /** Only registered routes create endpoint buckets; IDs and query filters share quotas. */
  8. final class RequestProfile
  9. {
  10. private const PAGED = [
  11. '/api/mini/v1/packages',
  12. '/api/mini/v1/enterprises/{id}/ledger',
  13. '/api/mini/v1/enterprises/{id}/orders',
  14. '/api/mini/v1/enterprises/{id}/devices',
  15. '/api/mini/v1/drivers/{id}/tasks',
  16. '/api/mini/v1/agents/{id}/customers',
  17. '/api/mini/v1/agents/{id}/orders',
  18. ];
  19. public static function endpoint(Request $request): string
  20. {
  21. $route = $request->route?->getPath();
  22. if (!$route || str_contains($route, '{path:')) {
  23. return 'unknown';
  24. }
  25. $method = $request->method() === 'HEAD' ? 'GET' : $request->method();
  26. // Route matching has already constrained non-ID segments to registered literals.
  27. return $method . ' ' . preg_replace('#/\d+(?=/|$)#', '/{id}', $request->path());
  28. }
  29. public static function listCost(Request $request): int
  30. {
  31. if ($request->method() !== 'GET') {
  32. return 0;
  33. }
  34. $path = preg_replace('#/\d+(?=/|$)#', '/{id}', $request->path());
  35. if (!in_array($path, self::PAGED, true)) {
  36. return 0;
  37. }
  38. return (new Pagination($request->get()))->pageSize;
  39. }
  40. public static function ip(Request $request): string
  41. {
  42. // Resolved through the proxy allowlist so a deployment behind Nginx keeps per-caller
  43. // buckets; an untrusted peer's own header is still ignored.
  44. $ip = ClientIp::resolve($request);
  45. $packed = @inet_pton($ip);
  46. if ($packed === false) {
  47. return 'unknown';
  48. }
  49. if (strlen($packed) === 16 && substr($packed, 0, 12) === str_repeat("\0", 10) . "\xff\xff") {
  50. $packed = substr($packed, 12);
  51. }
  52. return (string) inet_ntop($packed);
  53. }
  54. }