- 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>
170 lines
5.6 KiB
PHP
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®ion=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'],
|
|
],
|
|
]);
|
|
}
|
|
}
|