Initial commit

This commit is contained in:
2026-07-08 21:44:11 +02:00
commit 829e10437e
5 changed files with 220 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
state.json
config.php
+13
View File
@@ -0,0 +1,13 @@
# Supercardinality Online Services
The reference implementation for online services for Supercardinality.
## Setup
Create the `config.php` file based on `config.php.example`.
Host the repo root using a PHP capable web server.
It's a good idea to block access to your state file.
A common way to protect it from being publicly readable is to configure it to be outside of the web server.
If you store it in the default location you may want to block access to it using URL rewrite rules.
+7
View File
@@ -0,0 +1,7 @@
<?php
$state_file = "state.json"; // The location for the state file. If it does not exist, it will be created.
$error_reporting = false; // Whether PHP errors should be shown. Not recommended for production.
$lobby_timeout_seconds = 30; // How long it should take for unresponsive lobbies to get auto-delisted.
+57
View File
@@ -0,0 +1,57 @@
<?php
function enet_probe(string $host, int $port, float $timeout = 2.0): bool
{
$host = trim($host);
$addr = filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
? "[{$host}]"
: $host;
$sock = @stream_socket_client(
"udp://{$addr}:{$port}",
$errno, $errstr, $timeout, STREAM_CLIENT_CONNECT
);
if ($sock === false) return false;
stream_set_timeout($sock,
(int) floor($timeout),
(int) (fmod($timeout, 1.0) * 1_000_000)
);
$connectID = mt_rand(1, 0x7FFFFFFF);
// --- Protocol header (4 bytes, NO CRC32) ---
// 0x8FFF = SENT_TIME flag (0x8000) | peer 0xFFF (max/unassigned)
$header = pack('nn', 0x8FFF, 0);
// --- Command header (4 bytes) ---
$cmdHeader = pack('CCn', 0x82, 0xFF, 1);
// --- Connect body (44 bytes) ---
$cmdBody = pack('n', 0); // outgoingPeerID
$cmdBody .= pack('CC', 0xFF, 0xFF); // incomingSessionID, outgoingSessionID
$cmdBody .= pack('N', 1400); // mtu
$cmdBody .= pack('N', 65536); // windowSize
$cmdBody .= pack('N', 2); // channelCount
$cmdBody .= pack('N', 0); // incomingBandwidth
$cmdBody .= pack('N', 0); // outgoingBandwidth
$cmdBody .= pack('N', 5000); // packetThrottleInterval
$cmdBody .= pack('N', 2); // packetThrottleAcceleration
$cmdBody .= pack('N', 2); // packetThrottleDeceleration
$cmdBody .= pack('N', $connectID); // connectID
$cmdBody .= pack('N', 0); // data
$packet = $header . $cmdHeader . $cmdBody;
$written = @fwrite($sock, $packet);
if ($written !== strlen($packet)) {
fclose($sock);
return false;
}
$response = @fread($sock, 512);
$meta = stream_get_meta_data($sock);
fclose($sock);
return !$meta['timed_out'] && $response !== '' && $response !== false;
}
+141
View File
@@ -0,0 +1,141 @@
<?php
function generate_uuid_v4(): string {
$bytes = openssl_random_pseudo_bytes(16);
$bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40); // set version to 0100
$bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80); // set bits 6-7 to 10
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
}
if (file_exists("config.php")) {
include "config.php";
}
if (empty($state_file)) {
$state_file = "state.json";
}
if (empty($error_reporting)) {
$error_reporting = false;
}
if (empty($lobby_timeout_seconds)) {
$lobby_timeout_seconds = 30;
}
if ($error_reporting) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
}
if (!file_exists($state_file)) {
mkdir($state_file, 0777, true);
file_put_contents($state_file, json_encode([ "lobbies" => []]));
}
$state = json_decode(file_get_contents($state_file), true);
$cutoff_timestamp = time() - $lobby_timeout_seconds;
foreach (array_keys($state["lobbies"]) as $id) {
if ($state["lobbies"][$id]["updated_at"] < $cutoff_timestamp) {
unset($state["lobbies"][$id]);
}
}
if (isset($_GET["list"])) {
$list = [];
foreach ($state["lobbies"] as $id => $lobby) {
$list[$id] = [
"id" => $lobby["id"],
"host" => $lobby["host"],
"name" => $lobby["name"],
"creator" => $lobby["creator"],
"version" => $lobby["version"],
"created_at" => $lobby["created_at"],
];
}
header("Content-Type: application/json");
echo json_encode(["lobbies" => $list]);
}
elseif (isset($_GET["new"])) {
require "enet_probe.php";
$host = $_SERVER['REMOTE_ADDR'];
if (!empty($_GET["host"])) {
$host = $_GET["host"];
}
$reachable = enet_probe($host, 50403);
header("Content-Type: application/json");
if ($reachable) {
$id = generate_uuid_v4();
$secret = generate_uuid_v4();
$version = $_GET["version"] ?? "";
$creator = $_GET["creator"] ?? "";
$name = "{$creator}'s lobby";
if (!empty($_GET["name"])) {
$name = $_GET["name"];
}
$lobby = [
"id" => $id,
"secret" => $secret,
"host" => $host,
"name" => $name,
"version" => $version,
"creator" => $creator,
"updated_at" => time(),
"created_at" => time(),
];
$state["lobbies"][$id] = $lobby;
echo json_encode($lobby);
}
else {
http_response_code(424);
echo json_encode(["error" => "NOT_REACHABLE", "host" => $host]);
}
}
elseif (isset($_GET["update"])) {
$id = $_GET["id"];
$secret = $_GET["secret"];
$lobby = $state["lobbies"][$id];
header("Content-Type: application/json");
if (!in_array($id, array_keys($state["lobbies"])) || empty($lobby)) {
http_response_code(404);
echo json_encode(["error" => "NOT_FOUND", "id" => $id]);
}
elseif ($lobby["secret"] != $secret) {
http_response_code(403);
echo json_encode(["error" => "NOT_AUTHORIZED", "id" => $id]);
}
else {
$name = $_GET["name"] ?? "";
if (!empty($name)) {
$lobby["name"] = $name;
}
$lobby["updated_at"] = time();
$state["lobbies"][$id] = $lobby;
echo json_encode(["result" => "updated"]);
}
}
elseif (isset($_GET["close"])) {
$id = $_GET["id"];
$secret = $_GET["secret"];
$lobby = $state["lobbies"][$id];
header("Content-Type: application/json");
if (!in_array($id, array_keys($state["lobbies"])) || empty($lobby)) {
http_response_code(404);
echo json_encode(["error" => "NOT_FOUND", "id" => $id]);
}
elseif ($lobby["secret"] != $secret) {
http_response_code(403);
echo json_encode(["error" => "NOT_AUTHORIZED", "id" => $id]);
}
else {
unset($state["lobbies"][$id]);
echo json_encode(["result" => "closed"]);
}
}
else {
header("Content-Type: application/json");
echo json_encode(["status" => "OK"]);
}
file_put_contents($state_file, json_encode($state, JSON_PRETTY_PRINT));