<?phpnamespace App\Xapi\Fake;use Evenement\EventEmitter;/** * A connection to the demo pool's HTTP server (what a TcpConnection to port 80 of a host is): it reads one request and hands it to * $handler (FakeXapi's XvaOps), which answers through reply() / sendBody() as the frames go, at a bandwidth of $rate bytes a frame. * on('open'|'data'|'close'|'error'), write(), close(), poll(): the interface of the engine's streams. */final class FakeHttpStream extends EventEmitter{ public string $requestHead = ''; public string $method = ''; public string $path = ''; /** @var array<string, string> */ public array $headers = []; /** bytes of the request's body received (an upload) */ public float $received = 0.0; /** the first bytes of the body (ova.xml of an XVA) */ public string $bodyHead = ''; public bool $headDone = false; /** bytes of the answer handed to the client so far */ public float $sent = 0.0; /** what the handler keeps about this request (its task, the VM...) */ public array $context = []; private string $buffer = ''; private bool $opened = false; private bool $closed = false; /** @var list<string> what is to be sent, a bit each frame */ private array $out = []; private bool $closeAfter = false; /** @var \Closure(self, string): void called on 'head' (the request line and headers) and 'body' (a part arrived) */ private \Closure $handler; public int $rate = 393216; /** @param \Closure(self, string): void $handler */ public function __construct(\Closure $handler) { $this->handler = $handler; } public function write(string $bytes): bool { if ($this->closed) { return false; } $this->buffer .= $bytes; if (!$this->headDone) { $end = strpos($this->buffer, "\r\n\r\n"); if ($end === false) { return true; } $this->requestHead = substr($this->buffer, 0, $end); $this->buffer = (string)substr($this->buffer, $end + 4); $lines = explode("\r\n", $this->requestHead); [$this->method, $this->path] = explode(' ', (string)array_shift($lines)) + ['', '', '']; foreach ($lines as $line) { [$name, $value] = explode(':', $line, 2) + ['', '']; $this->headers[strtolower(trim($name))] = trim($value); } $this->headDone = true; ($this->handler)($this, 'head'); } if ($this->buffer !== '') { $this->received += strlen($this->buffer); if (strlen($this->bodyHead) < 262144) { $this->bodyHead .= substr($this->buffer, 0, 262144 - strlen($this->bodyHead)); } $this->buffer = ''; // the rest of an upload is counted, not kept ($this->handler)($this, 'body'); } return true; } /** An answer: the status line and headers now, the body by parts ($body: a string, or a list of strings sent one after the other). */ public function reply(int $status, string $reason, array $headers, string|array $body = '', bool $close = true): void { $head = "HTTP/1.1 $status $reason\r\n"; foreach ($headers as $name => $value) { $head .= "$name: $value\r\n"; } $this->out[] = $head . "\r\n"; foreach (is_array($body) ? $body : [$body] as $part) { if ($part !== '') { $this->out[] = $part; } } $this->closeAfter = $close; } public function close(): void { if ($this->closed) { return; } $this->closed = true; $this->emit('close', [null]); $this->removeAllListeners(); } public function isClosed(): bool { return $this->closed; } public function poll(): void { if ($this->closed) { return; } if (!$this->opened) { $this->opened = true; $this->emit('open'); return; } $budget = $this->rate; while ($this->out !== [] && $budget > 0 && !$this->isClosed()) { $part = $this->out[0]; $piece = substr($part, 0, $budget); $rest = (string)substr($part, strlen($piece)); $rest === '' ? array_shift($this->out) : $this->out[0] = $rest; $budget -= strlen($piece); $this->sent += strlen($piece); $this->emit('data', [$piece]); } if ($this->out === [] && $this->closeAfter && !$this->isClosed()) { $this->close(); } }}