Major additions: - Storage abstraction layer with filesystem and in-memory implementations - Gitea API integration with MCP tools for repository management - Console dialog mode with interactive command execution - WireGuard VPN DNS fix implementation and documentation - HTTP client streaming response support - Router generic result type - Parameter type validator for framework core Framework enhancements: - Console command registry improvements - Console dialog components - Method signature analyzer updates - Route mapper refinements - MCP server and tool mapper updates - Queue job chain and dependency commands - Discovery tokenizer improvements Infrastructure: - Deployment architecture documentation - Ansible playbook updates for WireGuard client regeneration - Production environment configuration updates - Docker Compose local configuration updates - Remove obsolete docker-compose.yml (replaced by environment-specific configs) Documentation: - PERMISSIONS.md for access control guidelines - WireGuard DNS fix implementation details - Console dialog mode usage guide - Deployment architecture overview Testing: - Multi-purpose attribute tests - Gitea Actions integration tests (typed and untyped)
276 lines
11 KiB
PHP
276 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\HttpClient;
|
|
|
|
use App\Framework\Http\HeaderManipulator;
|
|
use App\Framework\Http\Headers;
|
|
use App\Framework\Http\Status;
|
|
use App\Framework\HttpClient\Curl\Handle;
|
|
use App\Framework\HttpClient\Exception\CurlExecutionFailed;
|
|
use App\Framework\HttpClient\Exception\CurlNotInitialized;
|
|
|
|
final readonly class CurlHttpClient implements HttpClient
|
|
{
|
|
public function __construct(
|
|
private CurlResponseParser $responseParser = new CurlResponseParser(),
|
|
private AuthenticationHandler $authenticationHandler = new AuthenticationHandler(),
|
|
private CurlRequestBuilder $requestBuilder = new CurlRequestBuilder(),
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @throws CurlExecutionFailed
|
|
*/
|
|
public function send(ClientRequest $request): ClientResponse
|
|
{
|
|
// Initialize OOP curl handle
|
|
$handle = new Handle();
|
|
|
|
try {
|
|
// Build options using HandleOption enum
|
|
$options = $this->requestBuilder->buildOptions($request);
|
|
|
|
// Handle authentication
|
|
if ($request->options->auth !== null) {
|
|
$authResult = $this->authenticationHandler->configure($request->options->auth, $request->headers);
|
|
|
|
if ($authResult->headers !== $request->headers) {
|
|
$updatedRequest = $request->with(['headers' => $authResult->headers]);
|
|
$options = $this->requestBuilder->buildOptions($updatedRequest);
|
|
}
|
|
|
|
if (! empty($authResult->curlOptions)) {
|
|
$options = array_replace($options, $authResult->curlOptions);
|
|
}
|
|
}
|
|
|
|
// Set all options using fluent API
|
|
$handle->setOptions($options);
|
|
|
|
// Execute request (automatically uses CURLOPT_RETURNTRANSFER)
|
|
$rawResponse = $handle->fetch();
|
|
|
|
// Parse response using underlying CurlHandle resource
|
|
return $this->responseParser->parse($rawResponse, $handle->getResource());
|
|
} catch (\Throwable $e) {
|
|
// Wrap any exception in CurlExecutionFailed for backward compatibility
|
|
throw new CurlExecutionFailed($e->getMessage(), $e->getCode(), $e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send request with streaming response to destination
|
|
*
|
|
* Streams HTTP response directly to a writable stream resource.
|
|
* Useful for large file downloads without loading entire response into memory.
|
|
*
|
|
* @param ClientRequest $request HTTP request to send
|
|
* @param resource $destination Writable stream resource (e.g., fopen('file.txt', 'w'))
|
|
* @return StreamingResponse Response with headers and status, but no body (streamed to destination)
|
|
* @throws CurlExecutionFailed If request execution fails
|
|
*/
|
|
public function sendStreaming(ClientRequest $request, $destination): StreamingResponse
|
|
{
|
|
if (! is_resource($destination)) {
|
|
throw new \InvalidArgumentException('Destination must be a valid stream resource');
|
|
}
|
|
|
|
$handle = new Handle();
|
|
|
|
try {
|
|
// Build options using HandleOption enum
|
|
$options = $this->requestBuilder->buildOptions($request);
|
|
|
|
// Remove CURLOPT_RETURNTRANSFER (we're streaming to destination)
|
|
unset($options[\App\Framework\HttpClient\Curl\HandleOption::ReturnTransfer->value]);
|
|
|
|
// Handle authentication
|
|
if ($request->options->auth !== null) {
|
|
$authResult = $this->authenticationHandler->configure($request->options->auth, $request->headers);
|
|
|
|
if ($authResult->headers !== $request->headers) {
|
|
$updatedRequest = $request->with(['headers' => $authResult->headers]);
|
|
$options = $this->requestBuilder->buildOptions($updatedRequest);
|
|
unset($options[\App\Framework\HttpClient\Curl\HandleOption::ReturnTransfer->value]);
|
|
}
|
|
|
|
if (! empty($authResult->curlOptions)) {
|
|
$options = array_replace($options, $authResult->curlOptions);
|
|
}
|
|
}
|
|
|
|
// Enable header capture for streaming
|
|
$headerBuffer = '';
|
|
$headerFunction = function ($ch, $header) use (&$headerBuffer) {
|
|
$headerBuffer .= $header;
|
|
return strlen($header);
|
|
};
|
|
$options[\App\Framework\HttpClient\Curl\HandleOption::HeaderFunction->value] = $headerFunction;
|
|
|
|
// Set all options
|
|
$handle->setOptions($options);
|
|
|
|
// Execute and stream directly to destination
|
|
$handle->execute($destination);
|
|
|
|
// Parse response headers from buffer
|
|
$statusCode = $handle->getInfo(\App\Framework\HttpClient\Curl\Info::ResponseCode);
|
|
$status = \App\Framework\Http\Status::from((int) $statusCode);
|
|
$headers = $this->responseParser->parseHeaders($headerBuffer);
|
|
|
|
// Get bytes written (if available)
|
|
$bytesWritten = (int) ($handle->getInfo(\App\Framework\HttpClient\Curl\Info::SizeDownload) ?: 0);
|
|
|
|
return new StreamingResponse(
|
|
status: $status,
|
|
headers: $headers,
|
|
bytesWritten: $bytesWritten
|
|
);
|
|
} catch (\Throwable $e) {
|
|
throw new CurlExecutionFailed($e->getMessage(), $e->getCode(), $e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send request with streaming body from source
|
|
*
|
|
* Streams HTTP request body from a readable stream resource.
|
|
* Useful for large file uploads without loading entire file into memory.
|
|
*
|
|
* @param ClientRequest $request HTTP request to send (body will be replaced by stream)
|
|
* @param resource $source Readable stream resource (e.g., fopen('file.txt', 'r'))
|
|
* @param int|null $contentLength Content-Length in bytes (null for chunked transfer)
|
|
* @return ClientResponse HTTP response
|
|
* @throws CurlExecutionFailed If request execution fails
|
|
*/
|
|
public function sendStreamingUpload(ClientRequest $request, $source, ?int $contentLength = null): ClientResponse
|
|
{
|
|
if (! is_resource($source)) {
|
|
throw new \InvalidArgumentException('Source must be a valid stream resource');
|
|
}
|
|
|
|
$handle = new Handle();
|
|
|
|
try {
|
|
// Build options using HandleOption enum
|
|
$options = $this->requestBuilder->buildOptions($request);
|
|
|
|
// Remove CURLOPT_POSTFIELDS (we're using stream)
|
|
unset($options[\App\Framework\HttpClient\Curl\HandleOption::PostFields->value]);
|
|
|
|
// Set streaming upload options
|
|
$options[\App\Framework\HttpClient\Curl\HandleOption::Upload->value] = true;
|
|
$options[\App\Framework\HttpClient\Curl\HandleOption::InFile->value] = $source;
|
|
|
|
if ($contentLength !== null) {
|
|
$options[\App\Framework\HttpClient\Curl\HandleOption::InFileSize->value] = $contentLength;
|
|
}
|
|
|
|
// Handle authentication
|
|
if ($request->options->auth !== null) {
|
|
$authResult = $this->authenticationHandler->configure($request->options->auth, $request->headers);
|
|
|
|
if ($authResult->headers !== $request->headers) {
|
|
$updatedRequest = $request->with(['headers' => $authResult->headers]);
|
|
$options = array_replace($options, $this->requestBuilder->buildOptions($updatedRequest));
|
|
unset($options[\App\Framework\HttpClient\Curl\HandleOption::PostFields->value]);
|
|
$options[\App\Framework\HttpClient\Curl\HandleOption::Upload->value] = true;
|
|
$options[\App\Framework\HttpClient\Curl\HandleOption::InFile->value] = $source;
|
|
if ($contentLength !== null) {
|
|
$options[\App\Framework\HttpClient\Curl\HandleOption::InFileSize->value] = $contentLength;
|
|
}
|
|
}
|
|
|
|
if (! empty($authResult->curlOptions)) {
|
|
$options = array_replace($options, $authResult->curlOptions);
|
|
}
|
|
}
|
|
|
|
// Set all options
|
|
$handle->setOptions($options);
|
|
|
|
// Execute request (automatically uses CURLOPT_RETURNTRANSFER)
|
|
$rawResponse = $handle->fetch();
|
|
|
|
// Parse response
|
|
return $this->responseParser->parse($rawResponse, $handle->getResource());
|
|
} catch (\Throwable $e) {
|
|
throw new CurlExecutionFailed($e->getMessage(), $e->getCode(), $e);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
// URL mit Query-Parametern verarbeiten
|
|
$url = $request->url;
|
|
if (!empty($request->options->query)) {
|
|
$separator = str_contains($url, '?') ? '&' : '?';
|
|
$url .= $separator . http_build_query($request->options->query);
|
|
}
|
|
|
|
$options = [
|
|
CURLOPT_URL => $url,
|
|
CURLOPT_CUSTOMREQUEST => $request->method->value,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HEADER => true,
|
|
CURLOPT_TIMEOUT => $request->options->timeout,
|
|
CURLOPT_CONNECTTIMEOUT => $request->options->connectTimeout,
|
|
CURLOPT_FOLLOWLOCATION => $request->options->followRedirects,
|
|
CURLOPT_MAXREDIRS => $request->options->maxRedirects,
|
|
CURLOPT_SSL_VERIFYPEER => $request->options->verifySsl,
|
|
CURLOPT_SSL_VERIFYHOST => $request->options->verifySsl ? 2 : 0,
|
|
];
|
|
|
|
// User-Agent setzen wenn vorhanden
|
|
if ($request->options->userAgent !== null) {
|
|
$options[CURLOPT_USERAGENT] = $request->options->userAgent;
|
|
}
|
|
|
|
// Proxy setzen wenn vorhanden
|
|
if ($request->options->proxy !== null) {
|
|
$options[CURLOPT_PROXY] = $request->options->proxy;
|
|
}
|
|
|
|
// Authentifizierung einrichten
|
|
if ($request->options->auth !== null) {
|
|
$this->setupAuthentication($ch, $request->options->auth, $request->headers);
|
|
}
|
|
|
|
// Request-Body verarbeiten
|
|
if ($request->body !== '') {
|
|
$options[CURLOPT_POSTFIELDS] = $request->body;
|
|
}
|
|
|
|
// Headers formatieren und setzen
|
|
if (count($request->headers->all()) > 0) {
|
|
$options[CURLOPT_HTTPHEADER] = HeaderManipulator::formatForCurl($request->headers);
|
|
}
|
|
|
|
curl_setopt_array($ch, $options);
|
|
|
|
$raw = curl_exec($ch);
|
|
|
|
if ($raw === false) {
|
|
throw new CurlExecutionFailed(curl_error($ch), curl_errno($ch));
|
|
}
|
|
|
|
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
|
$headersRaw = substr($raw, 0, $headerSize);
|
|
$body = substr($raw, $headerSize);
|
|
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
$headers = HeaderManipulator::fromString($headersRaw);
|
|
|
|
return new ClientResponse(
|
|
status: Status::from($status),
|
|
headers: $headers,
|
|
body: $body
|
|
);
|
|
}
|
|
*/
|
|
}
|