lthn.io/app/Mod/Proxy/Controllers/ProxyController.php
Claude a7fa7ca087
feat(proxy): api.lthn.io proxy gateway module
- POST /v1/proxy/connect — get gateway node for service type (mobile/residential/seo)
- GET /v1/proxy/usage — usage tracking per API key (bytes, GB, requests)
- GET /v1/proxy/nodes — list available nodes by capability
- GET /v1/proxy/status — network availability + service pricing
- NodeSelector: round-robin selection from chain aliases by capability
- UsageMeter: per-key tracking of bytes and requests
- Three billing models: mobile ($5/GB), residential ($2.50/GB), SEO (per-request)
- Auth required for connect/usage, public for status/nodes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 04:33:13 +01:00

170 lines
5.6 KiB
PHP

<?php
declare(strict_types=1);
namespace Mod\Proxy\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Mod\Proxy\Services\NodeSelector;
use Mod\Proxy\Services\UsageMeter;
/**
* api.lthn.io proxy gateway API.
*
* POST /v1/proxy/connect — get a gateway connection for your service type
* GET /v1/proxy/usage — check your usage (bytes, requests, GB)
* GET /v1/proxy/nodes — list available nodes by capability
* GET /v1/proxy/status — network availability summary
*/
class ProxyController extends Controller
{
// Pricing per GB in USD (for display — actual billing via Blesta)
private const PRICING = [
'mobile' => 5.00,
'residential' => 2.50,
'seo' => 0.00, // per-request, not per-GB
];
public function __construct(
private readonly NodeSelector $selector,
private readonly UsageMeter $meter,
) {}
/**
* POST /v1/proxy/connect {"type": "residential"}
*
* Returns a gateway node to connect to for the requested service type.
* Requires API key (Bearer token from Blesta service).
*/
public function connect(Request $request): JsonResponse
{
$type = strtolower(trim((string) $request->input('type', 'proxy')));
$region = strtolower(trim((string) $request->input('region', '')));
// Map service types to node capabilities
$capMap = [
'mobile' => 'proxy',
'residential' => 'proxy',
'seo' => 'exit',
'vpn' => 'vpn',
'proxy' => 'proxy',
'dns' => 'dns',
];
$capability = $capMap[$type] ?? 'proxy';
$node = $this->selector->selectNode($capability);
if (! $node) {
return response()->json([
'error' => 'No nodes available for this service type.',
'type' => $type,
], 503);
}
// Track the request
$apiKey = $request->bearerToken() ?? 'anonymous';
$this->meter->recordRequest($apiKey);
// Parse node connection details from alias comment
$comment = $node['comment'] ?? '';
$caps = [];
if (preg_match('/cap=([^;]+)/', $comment, $m)) {
$caps = explode(',', $m[1]);
}
return response()->json([
'node' => $node['alias'] . '.lthn',
'address' => $node['address'] ?? '',
'type' => $type,
'capabilities' => $caps,
'pricing' => [
'model' => $type === 'seo' ? 'per-request' : 'per-gb',
'rate_usd' => self::PRICING[$type] ?? 0,
'currency' => 'LTHN',
],
'session' => substr(md5($apiKey . $node['alias'] . time()), 0, 16),
]);
}
/**
* GET /v1/proxy/usage
*
* Returns usage for the authenticated API key.
*/
public function usage(Request $request): JsonResponse
{
$apiKey = $request->bearerToken() ?? 'anonymous';
$usage = $this->meter->getUsage($apiKey);
return response()->json([
'api_key' => substr($apiKey, 0, 8) . '...',
'usage' => $usage,
'billing' => [
'mobile_cost' => round($usage['gb'] * self::PRICING['mobile'], 2),
'residential_cost' => round($usage['gb'] * self::PRICING['residential'], 2),
'seo_requests' => $usage['requests'],
],
]);
}
/**
* GET /v1/proxy/nodes?cap=proxy&region=eu
*
* List available nodes by capability.
*/
public function nodes(Request $request): JsonResponse
{
$capability = strtolower(trim((string) $request->get('cap', '')));
$nodes = $this->selector->availableNodes($capability);
$result = [];
foreach ($nodes as $node) {
$caps = [];
if (preg_match('/cap=([^;]+)/', $node['comment'] ?? '', $m)) {
$caps = explode(',', $m[1]);
}
$result[] = [
'name' => $node['alias'] . '.lthn',
'capabilities' => $caps,
'status' => 'online',
];
}
return response()->json([
'nodes' => $result,
'count' => count($result),
'filter' => $capability ?: 'all',
]);
}
/**
* GET /v1/proxy/status
*
* Network availability summary.
*/
public function status(): JsonResponse
{
$vpn = $this->selector->availableNodes('vpn');
$proxy = $this->selector->availableNodes('proxy');
$exit = $this->selector->availableNodes('exit');
$dns = $this->selector->availableNodes('dns');
return response()->json([
'status' => 'operational',
'availability' => [
'vpn' => ['nodes' => count($vpn), 'status' => count($vpn) > 0 ? 'available' : 'unavailable'],
'proxy' => ['nodes' => count($proxy), 'status' => count($proxy) > 0 ? 'available' : 'unavailable'],
'exit' => ['nodes' => count($exit), 'status' => count($exit) > 0 ? 'available' : 'unavailable'],
'dns' => ['nodes' => count($dns), 'status' => count($dns) > 0 ? 'available' : 'unavailable'],
],
'services' => [
'mobile_proxy' => ['available' => count($proxy) > 0, 'rate' => '$5.00/GB'],
'residential_proxy' => ['available' => count($proxy) > 0, 'rate' => '$2.50/GB'],
'seo_traffic' => ['available' => count($exit) > 0, 'rate' => 'per-request'],
],
]);
}
}