mince/src/MojangInterop.php

150 lines
5.7 KiB
PHP

<?php
namespace Mince;
use stdClass;
use Index\Http\{HttpResponseBuilder,HttpRequest};
use Index\Http\Routing\IRouter;
use Ramsey\Uuid\{Uuid,UuidInterface};
final class MojangInterop {
private const API_SERVER = 'https://api.mojang.com';
private const SESSION_SERVER = 'https://sessionserver.mojang.com';
public static function currentTime(): int {
return (int)(microtime(true) * 1000);
}
public static function nameUUIDFromBytes(string $data): UuidInterface {
$bytes = hash('md5', $data, true);
$bytes[6] = chr((ord($bytes[6]) & 0x0F) | 0x30); // set version 3
$bytes[8] = chr((ord($bytes[8]) & 0x3F) | 0x80); // set IETF variant
return Uuid::fromBytes($bytes);
}
public static function createOfflinePlayerUUID(string $name): UuidInterface {
return self::nameUUIDFromBytes(sprintf('OfflinePlayer:%s', $name));
}
public static function isOfflineId(UuidInterface $uuid): bool {
return $uuid->getVersion() === 3;
}
public static function isMojangId(UuidInterface $uuid): bool {
return $uuid->getVersion() === 4;
}
public static function registerRoutes(IRouter $router): void {
$router->get('/uuid', fn($response, $request) => self::uuidResolver($response, $request));
$router->get('/blockedservers', fn($response, $request) => self::proxyBlockServers($response, $request));
// figure out how to proxy these someday to keep online mode working transparently
$router->get('/session/minecraft/hasJoined', fn() => 501);
$router->post('/session/minecraft/join', fn() => 501);
}
public static function uuidResolver(HttpResponseBuilder $response, HttpRequest $request): string {
$response->setTypePlain();
$uuid = self::createOfflinePlayerUUID((string)$request->getParam('name'));
return (string)match((string)$request->getParam('mode')) {
'str' => $uuid->toString(),
'urn' => $uuid->getUrn(),
'raw' => $uuid->getBytes(),
'int' => $uuid->getInteger(),
default => $uuid->getHex(),
};
}
public static function getRequest(string $url, string $userAgent): object {
$out = new stdClass;
$curl = curl_init($url);
curl_setopt_array($curl, [
CURLOPT_AUTOREFERER => true,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TCP_NODELAY => true,
CURLOPT_HEADER => true,
CURLOPT_NOBODY => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => $userAgent,
]);
[$out->headers, $out->body] = explode("\r\n\r\n", curl_exec($curl));
curl_close($curl);
$out->headers = explode("\r\n", $out->headers);
$out->status = explode(' ', array_shift($out->headers), 3);
return $out;
}
public static function sessionServerGetRequest(string $path, string $userAgent): object {
return self::getRequest(self::SESSION_SERVER . $path, $userAgent);
}
public static function apiServerGetRequest(string $path, string $userAgent): object {
return self::getRequest(self::API_SERVER . $path, $userAgent);
}
public static function getBlockedServersRaw(string $userAgent): object {
return self::sessionServerGetRequest('/blockedservers', $userAgent);
}
public static function proxyBlockServers(HttpResponseBuilder $response, HttpRequest $request): string {
$info = self::getBlockedServersRaw($request->getHeaderLine('User-Agent'));
$response->setStatusCode((int)$info->status[1]);
$response->setCacheControl('max-age=300');
foreach($info->headers as $header) {
[$name, $value] = explode(':', $header);
$name = strtolower(trim($name));
if(str_starts_with($name, 'x-') || $name === 'content-type')
$response->setHeader($name, $value);
}
return $info->body;
}
public static function getMinecraftUUIDRaw(string $userName, string $userAgent): object {
return self::apiServerGetRequest(sprintf('/users/profiles/minecraft/%s', $userName), $userAgent);
}
public static function getMinecraftUUID(string $userName, string $userAgent): ?object {
$info = self::getMinecraftUUIDRaw($userName, $userAgent);
if($info->status[1] !== '200')
return null;
return json_decode($info->body);
}
public static function getSessionMinecraftProfileRaw(string $uuid, string $userAgent): object {
return self::sessionServerGetRequest(sprintf('/session/minecraft/profile/%s', $uuid), $userAgent);
}
public static function getSessionMinecraftProfile(string $uuid, string $userAgent): ?object {
$info = self::getSessionMinecraftProfileRaw($uuid, $userAgent);
if($info->status[1] !== '200')
return null;
return json_decode($info->body);
}
public static function proxySessionMinecraftProfile(HttpResponseBuilder $response, HttpRequest $request, string $uuid): string {
$info = self::getSessionMinecraftProfileRaw($uuid, $request->getHeaderLine('User-Agent'));
$response->setStatusCode((int)$info->status[1]);
$response->setCacheControl('max-age=30');
foreach($info->headers as $header) {
[$name, $value] = explode(':', $header);
$name = strtolower(trim($name));
if(str_starts_with($name, 'x-') || $name === 'content-type')
$response->setHeader($name, $value);
}
return $info->body;
}
}