Rewrote remote interaction thing.

This commit is contained in:
flash 2023-02-25 20:17:06 +00:00
parent c6026897ed
commit f322be94b4
9 changed files with 223 additions and 320 deletions

View file

@ -3,6 +3,7 @@ namespace Mince;
use Index\Autoloader;
use Index\Environment;
use Index\XString;
use Index\Data\ConnectionFailedException;
use Index\Data\DbTools;
@ -31,11 +32,11 @@ try {
die($ex->getMessage());
}
Remote::setUrl($config['remote_url']);
Remote::setSecret($config['remote_secret']);
$remote = new RemoteV2($config['remotev2_url'], $config['remotev2_secret']);
if(PHP_SAPI !== 'cli') {
if(empty($_COOKIE['mc_random'])) {
$sVerification = Utils::generatePassKey(32);
$sVerification = XString::random(32);
setcookie('mc_random', $sVerification, strtotime('1 day'), '/', $_SERVER['HTTP_HOST']);
} else
$sVerification = (string)filter_input(INPUT_COOKIE, 'mc_random');
@ -44,3 +45,4 @@ $sVerification = hash('sha256', $sVerification);
// replace this with id.flashii.net shit
$userInfo = ChatAuth::attempt($db, $config['chat_endpoint'], $config['chat_secret'], (string)filter_input(INPUT_COOKIE, 'msz_auth'));
}

View file

@ -8,12 +8,8 @@ use Mince\HTML;
require_once __DIR__ . '/../mince.php';
$timing = new Timings;
$router = new HttpFx;
$timing->lap('httpfx', 'HttpFx Created');
$loginUrl = $config['login_url'];
$router->setDefaultErrorHandler(function($response, $request, $code, $text) use ($loginUrl, $userInfo) {
@ -26,19 +22,15 @@ $router->setDefaultErrorHandler(function($response, $request, $code, $text) use
$response->setContent($body);
});
$router->use('/', function($response) use ($timing) {
$response->setPoweredBy('Mince+Index');
$response->setServerTiming($timing);
$router->use('/', function($response) {
$response->setPoweredBy('Mince');
});
$router->get('/index.php', function($response) {
$response->redirect('/', true);
});
$router->get('/map.php', function($response) {
$response->redirect('/maps/survival', true);
});
$router->get('/', function($response, $request) use ($db, $loginUrl, $userInfo, $sVerification) {
$router->get('/', function($response, $request) use ($db, $remote, $loginUrl, $userInfo, $sVerification) {
$name = (string)$request->getParam('name');
$error = (string)$request->getParam('error');
@ -79,12 +71,12 @@ $router->get('/', function($response, $request) use ($db, $loginUrl, $userInfo,
$body .= ' <h2>Add to Whitelist</h2>';
$body .= ' <p>This will give you access to the server.</p>';
$body .= ' <form method="post" action="/whitelist/add">';
$body .= sprintf(' <input type="hidden" name="boob" value="%s"/>', $sVerification);
$body .= sprintf(' <input type="hidden" name="boob" value="%s">', $sVerification);
$body .= ' <label>';
$body .= ' <div class="label-header">Username</div>';
$body .= sprintf(' <div class="label-input"><input type="text" name="name" value="%s" /></div>', htmlspecialchars($name));
$body .= sprintf(' <div class="label-input"><input type="text" name="name" value="%s"></div>', htmlspecialchars($name));
$body .= ' </label>';
$body .= ' <input type="submit" value="Add me to the Whitelist"/>';
$body .= ' <input type="submit" value="Add me to the Whitelist">';
$body .= ' </form>';
$body .= '</div>';
}
@ -113,8 +105,8 @@ $router->get('/', function($response, $request) use ($db, $loginUrl, $userInfo,
$body .= ' <p>This will revoke your access to the server.</p>';
$body .= sprintf(' <p>You are currently whitelisted as <b>%s</b> on <b>%s</b>.</p>', $userInfo->mc_username, date('Y-m-d H:i:s T', $userInfo->mc_whitelisted));
$body .= ' <form method="post" action="/whitelist/remove">';
$body .= sprintf(' <input type="hidden" name="boob" value="%s"/>', $sVerification);
$body .= ' <input type="submit" value="Remove me from the whitelist"/>';
$body .= sprintf(' <input type="hidden" name="boob" value="%s">', $sVerification);
$body .= ' <input type="submit" value="Remove me from the whitelist">';
$body .= ' </form>';
$body .= '</div>';
}
@ -146,21 +138,6 @@ $router->get('/', function($response, $request) use ($db, $loginUrl, $userInfo,
return $body;
});
$router->get('/maps', function($response) {
$response->redirect('/maps/survival');
});
$router->get('/maps/survival', function($response) use ($loginUrl, $userInfo) {
$body = HTML::getHeader($userInfo, $loginUrl);
$body .= '<div style="text-align: center">';
$body .= ' <iframe src="//mc-survival-map.mikoto.best/" width="854" height="480" allow="fullscreen" allowfullscreen style="border-width: 0;"></iframe>';
$body .= '</div>';
$body .= HTML::getFooter();
return $body;
});
$router->use('/whitelist', function($response, $request) use ($sVerification) {
if(!$request->isFormContent()) {
$response->redirect('/?error=request');
@ -183,7 +160,7 @@ $router->post('/whitelist/add', function($response, $request) use ($db, $userInf
$body = $request->getContent();
$name = (string)$body->getParam('name');
$resp = Whitelist::add($db, $userInfo, $name);
$resp = (new Whitelist($db))->add($userInfo, $name);
if($resp === '')
$response->redirect('/');
@ -195,7 +172,7 @@ $router->post('/whitelist/add', function($response, $request) use ($db, $userInf
});
$router->post('/whitelist/remove', function($response) use ($db, $userInfo) {
$resp = Whitelist::remove($db, $userInfo);
$resp = (new Whitelist($db))->remove($userInfo);
if($resp === '')
$response->redirect('/');
@ -203,35 +180,6 @@ $router->post('/whitelist/remove', function($response) use ($db, $userInfo) {
$response->redirect("/?error={$resp}");
});
$router->get('/status', function($response) {
$response->redirect('/status/survival');
});
$router->get('/status/survival', function() {
return '<!doctype html>todo: make something here';
});
$router->get('/status/survival.json', function() {
return ServerQuery::create('mc.flashii.net')->stats();
});
$router->get('/status/survival.png', function($response) {
$stats = ServerQuery::create('mc.flashii.net')->stats();
$image = new \Imagick;
$image->newImage(100, 100, 'black', 'png');
$draw = new \ImagickDraw;
$draw->setFillColor('white');
$image->annotateImage($draw, 10, 10, 0, $stats->motd);
$response->setContentType('image/png');
$response->setContent((string)$image);
$image->destroy();
});
$router->get('/errors/:code', function($res, $req, $code) {
$code = intval($code);
if($code < 100 || $code >= 600)
@ -239,6 +187,4 @@ $router->get('/errors/:code', function($res, $req, $code) {
return $code;
});
$timing->lap('routes');
$router->dispatch();

View file

@ -13,16 +13,16 @@ final class HTML {
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta charset="utf-8">
<title>{$title}</title>
<link href="/mince.css" type="text/css" rel="stylesheet"/>
<link href="/mince.css" type="text/css" rel="stylesheet">
</head>
<body>
<div class="wrapper">
<nav class="header">
<div class="header-inner">
<div class="header-logo">
<a href="/"><img src="/assets/weblogo.png" alt="Flashii Minecraft"/></a>
<a href="/"><img src="/assets/weblogo.png" alt="Flashii Minecraft"></a>
</div>
<div class="header-fat"></div>
<div class="header-user">
@ -38,7 +38,7 @@ HTML;
return <<<HTML
</div>
<footer class="footer">
<a href="https://flash.moe">Flashwave</a> 2022 | Site design "borrowed" from pre-Microsoft Mojang | "Minecraft" is a trademark of Mojang
<a href="https://flash.moe">Flashwave</a> 2022-2023 | Site design "borrowed" from pre-Microsoft Mojang | "Minecraft" is a trademark of Mojang
</footer>
</div>
</body>

View file

@ -1,45 +0,0 @@
<?php
namespace Mince;
final class Remote {
private static string $url = '';
private static string $secret = '';
public static function setUrl(string $url): void {
self::$url = $url;
}
public static function setSecret(string $secret): void {
self::$secret = $secret;
}
public static function call(string $mode, string $name): string {
$time = (string)floor(time() / 120);
$sign = hash_hmac('sha256', "{$time}#{$mode}#{$name}", self::$secret, true);
$request = curl_init(self::$url);
curl_setopt_array($request, [
CURLOPT_AUTOREFERER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HEADER => false,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'm' => $mode,
's' => $sign,
'n' => $name,
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_MAXREDIRS => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'mc.flashii.net',
]);
$response = curl_exec($request);
curl_close($request);
return $response;
}
}

131
src/RemoteV2.php Normal file
View file

@ -0,0 +1,131 @@
<?php
namespace Mince;
use RuntimeException;
use Index\Serialisation\Serialiser;
class RemoteV2 {
public function __construct(
private string $endPoint,
private string $secretKey
) {}
public function getInfo(): object {
return $this->getRequest('GET', '/');
}
public function getWhitelist(string $serverId): object {
return $this->getRequest('GET', '/whitelist', ['server' => $serverId]);
}
public function addToWhitelist(string $serverId, array $userNames): object {
return $this->postRequest('POST', '/whitelist', [
'server' => $serverId,
'names' => json_encode($userNames),
]);
}
public function removeFromWhitelist(string $serverId, array $userNames): object {
return $this->getRequest('DELETE', '/whitelist', [
'server' => $serverId,
'names' => json_encode($userNames),
]);
}
public function createSignature(string $method, string $path, array $params, int $time = -1): string {
if($time < 0)
$time = time();
ksort($params);
$compare = [];
// other sides supports arrays, not gonna bother here
foreach($params as $name => $value)
$compare[] = "{$name}:{$value}";
$input = "{$time}%{$method} {$path}%" . implode('#', $compare);
return Serialiser::uriBase64()->serialise(
hash_hmac('sha256', $input, $this->secretKey, true)
);
}
public function getRequest(string $method, string $path, array $params = []): mixed {
$time = time();
$sign = $this->createSignature($method, $path, $params, $time);
$url = $this->endPoint . $path;
if(!empty($params))
$url .= '?' . http_build_query($params);
$request = curl_init($url);
curl_setopt_array($request, [
CURLOPT_AUTOREFERER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HEADER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_MAXREDIRS => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_USERAGENT => 'mc.flashii.net',
CURLOPT_HTTPHEADER => [
"X-Mince-Signature: {$sign}",
"X-Mince-Timestamp: {$time}",
],
]);
$response = curl_exec($request);
curl_close($request);
if(empty($response))
throw new RuntimeException('Empty response.');
$response = json_decode($response);
if(!empty($response->error))
throw new RuntimeException($response->error);
return $response;
}
public function postRequest(string $method, string $path, array $params): mixed {
$time = time();
$sign = $this->createSignature($method, $path, $params, $time);
$url = $this->endPoint . $path;
$request = curl_init($url);
curl_setopt_array($request, [
CURLOPT_AUTOREFERER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HEADER => false,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $params,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_MAXREDIRS => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_USERAGENT => 'mc.flashii.net',
CURLOPT_HTTPHEADER => [
"X-Mince-Signature: {$sign}",
"X-Mince-Timestamp: {$time}",
],
]);
$response = curl_exec($request);
curl_close($request);
if(empty($response))
throw new RuntimeException('Empty response.');
$response = json_decode($response);
if(!empty($response->error))
throw new RuntimeException($response->error);
return $response;
}
}

View file

@ -1,166 +0,0 @@
<?php
namespace Mince;
use stdClass;
use RuntimeException;
use Socket;
use Index\AString;
use Index\Net\EndPoint;
use Index\Net\IPAddress;
use Index\Net\IPEndPoint;
use Index\Net\DnsEndPoint;
class ServerQuery { // rewrite this to use https://wiki.vg/Server_List_Ping query is kinda useless
public const PORT = 25565;
private string $addr;
private int $port;
private Socket $socket;
private int $sessionId;
private int $challengeToken = 0;
public function __construct(IPAddress $addr, int $port) {
$this->addr = (string)$addr;
$this->port = $port;
$this->sessionId = random_int(0, 0x7FFFFFFF) & 0x0F0F0F0F;
$this->socket = socket_create($addr->isV6() ? AF_INET6 : AF_INET, SOCK_DGRAM, SOL_UDP);
$this->handshake();
}
public function __destruct() {
socket_close($this->socket);
}
public function handshake(): void {
$response = $this->send(9);
$length = strlen($response);
$token = '';
for($i = 0; $i < $length; ++$i) {
$char = $response[$i];
if($char === "\0")
break;
$token .= $char;
}
$this->challengeToken = intval($token);
}
public function stats(): object {
$response = $this->send(0, pack('N', $this->challengeToken));
$offset = 0;
$data = new stdClass;
$data->motd = self::readString($response, $offset);
$data->gametype = self::readString($response, $offset);
$data->map = self::readString($response, $offset);
$data->numplayers = self::readString($response, $offset);
$data->maxplayers = self::readString($response, $offset);
$data->hostport = unpack('v', substr($response, $offset, 2))[1];
$offset += 2;
$data->hostip = self::readString($response, $offset);
return $data;
}
private static function readString(string $source, int &$offset): string {
$length = strlen($source);
$string = '';
for(; $offset < $length; ++$offset) {
$char = $source[$offset];
if($char === "\0")
break;
$string .= $char;
}
++$offset;
return $string;
}
private function send(int $type, string $payload = ''): string {
$payload = "\xFE\xFD" . pack('CN', $type, $this->sessionId) . $payload;
socket_sendto($this->socket, $payload, strlen($payload), 0, $this->addr, $this->port);
socket_recv($this->socket, $response, 1024, MSG_WAITALL);
$data = unpack('Ctype/Nsession', $response);
if($data['type'] !== $type)
throw new RuntimeException('Type does not match.');
if($data['session'] !== $this->sessionId)
throw new RuntimeException('Session id does not match.');
return substr($response, 5);
}
public static function create(AString|string $endPoint): ServerQuery {
$endPoint = AString::cast($endPoint);
$firstChar = $endPoint[0];
if($firstChar === '[') { // IPv6
if($endPoint->contains(']:'))
$endPoint = IPEndPoint::parse($endPoint);
else
$endPoint = new IPEndPoint(IPAddress::parse($endPoint->trim('[]')), self::PORT);
return new ServerQuery($endPoint->getAddress(), $endPoint->getPort());
} elseif(is_numeric($firstChar)) { // IPv4
if($endPoint->contains(':'))
$endPoint = IPEndPoint::parse($endPoint);
else
$endPoint = new IPEndPoint(IPAddress::parse($endPoint), self::PORT);
return new ServerQuery($endPoint->getAddress(), $endPoint->getPort());
} else { // DNS
if($endPoint->contains(':'))
$endPoint = DnsEndPoint::parse($endPoint);
else {
$endPoint = new DnsEndPoint($endPoint, self::PORT);
$records = dns_get_record('_minecraft._tcp.' . (string)$endPoint->getHost(), DNS_SRV);
if(!empty($records)) {
usort($records, function($a, $b) {
$priority = $a['pri'] <=> $b['pri'];
if($priority !== 0)
return $priority;
$weight = $a['weight'] <=> $b['weight'];
if($weight !== 0)
return $priority;
return 0;
});
foreach($records as $record) {
try {
return ServerQuery::create($record['target'] . ':' . $record['port']);
} catch(Exception $ex) {}
}
}
}
$records = dns_get_record((string)$endPoint->getHost(), DNS_A);
if(!empty($records)) {
foreach($records as $record) {
try {
return new ServerQuery(IPAddress::parse($record['ip']), $endPoint->getPort());
} catch(Exception $ex) {}
}
}
$records = dns_get_record((string)$endPoint->getHost(), DNS_AAAA);
if(!empty($records)) {
foreach($records as $record) {
try {
return new ServerQuery(IPAddress::parse($record['ipv6']), $endPoint->getPort());
} catch(Exception $ex) {}
}
}
}
throw new RuntimeException('Failed to connect.');
}
}

View file

@ -1,14 +0,0 @@
<?php
namespace Mince;
final class Utils {
private const CHARS = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789';
public static function generatePassKey(int $length): string {
$keyChars = strlen(self::CHARS) - 1;
$bytes = str_repeat("\0", $length);
for($i = 0; $i < $length; ++$i)
$bytes[$i] = self::CHARS[random_int(0, $keyChars)];
return $bytes;
}
}

View file

@ -4,8 +4,22 @@ namespace Mince;
use Index\Data\IDbConnection;
use Index\Data\DbType;
final class Whitelist {
public static function add(IDbConnection $db, object $userInfo, string $userName): string {
class Whitelist {
public function __construct(
private IDbConnection $dbConn
) {}
public function getNames(): array {
$names = [];
$getNames = $this->dbConn->query('SELECT minecraft_username FROM whitelist_2022');
while($getNames->next())
$names[] = $getNames->getString(0);
return $names;
}
public function add(object $userInfo, string $userName): string {
$length = strlen($userName);
if($length < 3)
return 'short';
@ -14,7 +28,7 @@ final class Whitelist {
if(!preg_match('#^([a-zA-Z0-9_]{3,16})$#', $userName))
return 'invalid';
$dupeCheck = $db->prepare('SELECT COUNT(`flashii_id`) > 0 FROM `whitelist_2022` WHERE `minecraft_username` = ?');
$dupeCheck = $this->dbConn->prepare('SELECT COUNT(`flashii_id`) > 0 FROM `whitelist_2022` WHERE `minecraft_username` = ?');
$dupeCheck->addParameter(1, $userName, DbType::STRING);
$dupeCheck->execute();
$dupeResult = $dupeCheck->getResult();
@ -25,17 +39,12 @@ final class Whitelist {
return 'conflict';
if(!empty($userInfo->mc_whitelisted) || !empty($userInfo->mc_username)) {
$resp = self::remove($db, $userInfo);
$resp = $this->remove($userInfo);
if($resp !== '')
return $resp;
}
$resp = Remote::call('wl:add', $userName);
if($resp !== 'success')
return $resp;
$insert = $db->prepare('INSERT INTO `whitelist_2022` (`flashii_id`, `minecraft_username`) VALUES (?, ?)');
$insert = $this->dbConn->prepare('INSERT INTO `whitelist_2022` (`flashii_id`, `minecraft_username`) VALUES (?, ?)');
$insert->addParameter(1, $userInfo->user_id);
$insert->addParameter(2, $userName, DbType::STRING);
$insert->execute();
@ -46,16 +55,11 @@ final class Whitelist {
return '';
}
public static function remove(IDbConnection $db, object $userInfo): string {
public function remove(object $userInfo): string {
if(empty($userInfo->mc_whitelisted) || empty($userInfo->mc_username))
return 'not-listed';
$resp = Remote::call('wl:remove', $userInfo->mc_username);
if($resp !== 'success')
return $resp;
$delete = $db->prepare('DELETE FROM `whitelist_2022` WHERE `flashii_id` = ?');
$delete = $this->dbConn->prepare('DELETE FROM `whitelist_2022` WHERE `flashii_id` = ?');
$delete->addParameter(1, $userInfo->user_id);
$delete->execute();

45
tools/sync Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env php
<?php
use Mince\Whitelist;
require_once __DIR__ . '/../mince.php';
echo 'Syncing server whitelists...' . PHP_EOL;
$rInfo = $remote->getInfo();
if(empty($rInfo->servers)) {
echo 'There are no active servers.' . PHP_EOL;
return;
}
echo 'Fetching master list from database...' . PHP_EOL;
$myNames = (new Whitelist($db))->getNames();
foreach($rInfo->servers as $serverId) {
try {
echo "[{$serverId}] Fetching list on server..." . PHP_EOL;
$rWhitelist = $remote->getWhitelist($serverId);
$rNames = $rWhitelist->list ?? [];
echo "[{$serverId}] Filtering..." . PHP_EOL;
$addNames = array_values(array_udiff($myNames, $rNames, 'strcasecmp'));
$removeNames = array_values(array_udiff($rNames, $myNames, $addNames, 'strcasecmp'));
if(!empty($addNames)) {
echo "[{$serverId}] Adding names..." . PHP_EOL;
$addResult = $remote->addToWhitelist($serverId, $addNames);
foreach($addResult->results as $name => $result)
echo "[{$serverId}] [{$name}] {$result->message}" . PHP_EOL;
}
if(!empty($removeNames)) {
echo "[{$serverId}] Removing names..." . PHP_EOL;
$removeResult = $remote->removeFromWhitelist($serverId, $removeNames);
foreach($removeResult->results as $name => $result)
echo "[{$serverId}] [{$name}] {$result->message}" . PHP_EOL;
}
} catch(RuntimeException $ex) {
var_dump((string)$ex);
echo PHP_EOL;
}
}