Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
state.json
|
||||
config.php
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
Reference in New Issue
Block a user