Skip to content

Commit dc48dc0

Browse files
committed
fix: allow tcp initial packet rewrite
1 parent 437d454 commit dc48dc0

3 files changed

Lines changed: 48 additions & 1 deletion

File tree

src/Adapter/TCP.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Swoole\Coroutine\Client;
66
use Utopia\Proxy\Adapter;
7+
use Utopia\Proxy\ConnectionResult;
78
use Utopia\Proxy\Protocol;
89
use Utopia\Proxy\Resolver;
910
use Utopia\Proxy\Sockmap\Loader as Sockmap;
@@ -40,9 +41,14 @@ class TCP extends Adapter
4041
/** @var array<int, Client> */
4142
protected array $connections = [];
4243

44+
/** @var array<int, string> */
45+
protected array $initialData = [];
46+
4347
/** @var array<int, int> client fd => backend fd, for sockmap pairs handed to the kernel */
4448
protected array $sockmapPairs = [];
4549

50+
protected ?\Closure $initialDataTransformer = null;
51+
4652
protected float $timeout = 30.0;
4753

4854
protected float $connectTimeout = 5.0;
@@ -66,6 +72,25 @@ public function __construct(
6672
parent::__construct($resolver);
6773
}
6874

75+
/**
76+
* Transform the first client packet before it is forwarded to the backend.
77+
*
78+
* The callback receives the raw initial packet and route result. This lets
79+
* protocol-aware callers route by one identifier while forwarding a backend
80+
* specific startup packet.
81+
*/
82+
public function onInitialData(callable $callback): static
83+
{
84+
$this->initialDataTransformer = $callback(...);
85+
86+
return $this;
87+
}
88+
89+
public function getInitialData(int $fd, string $default): string
90+
{
91+
return $this->initialData[$fd] ?? $default;
92+
}
93+
6994
public function setTimeout(float $timeout): static
7095
{
7196
$this->timeout = $timeout;
@@ -181,6 +206,7 @@ public function getConnection(string $data, int $fd): Client
181206

182207
$result = $this->route($data);
183208
[$host, $port] = self::parseEndpoint($result->endpoint, $this->port);
209+
$initialData = $this->transformInitialData($data, $result);
184210

185211
$client = new Client(SWOOLE_SOCK_TCP);
186212
$client->set([
@@ -196,10 +222,25 @@ public function getConnection(string $data, int $fd): Client
196222

197223
$this->applySocketOptions($client);
198224
$this->connections[$fd] = $client;
225+
$this->initialData[$fd] = $initialData;
199226

200227
return $client;
201228
}
202229

230+
protected function transformInitialData(string $data, ConnectionResult $result): string
231+
{
232+
if ($this->initialDataTransformer === null) {
233+
return $data;
234+
}
235+
236+
$transformed = ($this->initialDataTransformer)($data, $result);
237+
if (!\is_string($transformed)) {
238+
throw new \Exception('Initial data transformer must return string');
239+
}
240+
241+
return $transformed;
242+
}
243+
203244
/**
204245
* Hand the (client fd, backend fd) pair to the kernel via sockmap.
205246
*
@@ -270,6 +311,7 @@ public function closeConnection(int $fd): void
270311
}
271312

272313
unset($this->connections[$fd]);
314+
unset($this->initialData[$fd]);
273315
$client->close();
274316
}
275317

src/Server/TCP/Swoole.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ public function onReceive(Server $server, int $fd, string $data, int $port): voi
344344
// sockmap — once the pair is in the map, any send() on the
345345
// backend fd would be redirected back to the client by the
346346
// sk_msg program.
347-
$backend->send($data);
347+
$backend->send($adapter->getInitialData($fd, $data));
348348

349349
// Activate kernel zero-copy relay. If successful the kernel
350350
// handles all subsequent bytes and we skip the userspace

tests/Integration/TCPServerTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,23 @@ public function testTCPAdapterGetConnectionAndClose(): void
5959
$adapter->setSkipValidation(true);
6060
$adapter->setTimeout(5.0);
6161
$adapter->setConnectTimeout(2.0);
62+
$adapter->onInitialData(function (string $data): string {
63+
return str_replace('routing', 'backend', $data);
64+
});
6265

6366
// getConnection dials the backend and caches by fd
6467
$client = $adapter->getConnection('routing-data', 1);
6568
$this->assertInstanceOf(Client::class, $client);
6669
$this->assertTrue($client->isConnected());
70+
$this->assertSame('backend-data', $adapter->getInitialData(1, 'routing-data'));
6771

6872
// Same fd returns cached connection
6973
$cached = $adapter->getConnection('ignored', 1);
7074
$this->assertSame($client, $cached);
7175

7276
// closeConnection cleans up
7377
$adapter->closeConnection(1);
78+
$this->assertSame('fallback', $adapter->getInitialData(1, 'fallback'));
7479

7580
// Closing again is a no-op
7681
$adapter->closeConnection(1);

0 commit comments

Comments
 (0)