2023-08-22 23:47:37 +00:00
|
|
|
<?php
|
|
|
|
namespace Mince;
|
|
|
|
|
|
|
|
use stdClass;
|
|
|
|
use Index\Http\HttpResponseBuilder;
|
|
|
|
use Index\Http\HttpRequest;
|
|
|
|
use Index\Routing\IRouter;
|
|
|
|
use Ramsey\Uuid\Uuid;
|
|
|
|
use Ramsey\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 {
|
2023-08-23 18:06:47 +00:00
|
|
|
$router->get('/uuid', fn($response, $request) => self::uuidResolver($response, $request));
|
2023-08-22 23:47:37 +00:00
|
|
|
$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);
|
|
|
|
}
|
|
|
|
|
2023-08-23 18:06:47 +00:00
|
|
|
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(),
|
|
|
|
};
|
2023-08-22 23:47:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|