Introduce support for Translatable disconnection messages

this allows localizing disconnection screens (at least, once #4512 has been addressed) and the disconnect reasons shown on the console.

We already had disconnect messages implicitly localized in a few places, so this is just formalizing it.
This does break BC with any code that previously passed translation keys as the disconnect screen message, because they'll no longer be translated (only Translatables will be translatated now).
This commit is contained in:
Dylan K. Taylor
2022-12-27 18:36:07 +00:00
parent 9796dfd4d9
commit f173b91ca1
11 changed files with 86 additions and 55 deletions

View File

@ -23,7 +23,8 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
use pocketmine\lang\KnownTranslationKeys;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\protocol\types\login\JwtChainLinkBody;
@ -49,7 +50,7 @@ class ProcessLoginTask extends AsyncTask{
* keychain is invalid for whatever reason (bad signature, not in nbf-exp window, etc). If this is non-null, the
* keychain might have been tampered with. The player will always be disconnected if this is non-null.
*/
private ?string $error = "Unknown";
private Translatable|string|null $error = "Unknown";
/**
* Whether the player is logged into Xbox Live. This is true if any link in the keychain is signed with the Mojang
* root public key.
@ -59,7 +60,7 @@ class ProcessLoginTask extends AsyncTask{
/**
* @param string[] $chainJwts
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPublicKey) : void $onCompletion
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion
*/
public function __construct(
array $chainJwts,
@ -76,7 +77,7 @@ class ProcessLoginTask extends AsyncTask{
$this->clientPublicKey = $this->validateChain();
$this->error = null;
}catch(VerifyLoginException $e){
$this->error = $e->getMessage();
$this->error = $e->getDisconnectMessage();
}
}
@ -109,7 +110,8 @@ class ProcessLoginTask extends AsyncTask{
try{
[$headersArray, $claimsArray, ] = JwtUtils::parse($jwt);
}catch(JwtException $e){
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), 0, $e);
//TODO: we shouldn't be showing internal information like this to the client
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
}
$mapper = new \JsonMapper();
@ -121,21 +123,23 @@ class ProcessLoginTask extends AsyncTask{
/** @var JwtHeader $headers */
$headers = $mapper->map($headersArray, new JwtHeader());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), 0, $e);
//TODO: we shouldn't be showing internal information like this to the client
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e);
}
$headerDerKey = base64_decode($headers->x5u, true);
if($headerDerKey === false){
//TODO: we shouldn't be showing internal information like this to the client
throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u");
}
if($currentPublicKey === null){
if(!$first){
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_MISSINGKEY);
throw new VerifyLoginException("Missing JWT public key", KnownTranslationFactory::pocketmine_disconnect_invalidSession_missingKey());
}
}elseif($headerDerKey !== $currentPublicKey){
//Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE);
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
try{
@ -145,10 +149,10 @@ class ProcessLoginTask extends AsyncTask{
}
try{
if(!JwtUtils::verify($jwt, $signingKeyOpenSSL)){
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE);
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
}catch(JwtException $e){
throw new VerifyLoginException($e->getMessage(), 0, $e);
throw new VerifyLoginException($e->getMessage(), null, 0, $e);
}
if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY){
@ -164,16 +168,16 @@ class ProcessLoginTask extends AsyncTask{
/** @var JwtChainLinkBody $claims */
$claims = $mapper->map($claimsArray, new JwtChainLinkBody());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), 0, $e);
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
}
$time = time();
if(isset($claims->nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOEARLY);
throw new VerifyLoginException("JWT not yet valid", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooEarly());
}
if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOLATE);
throw new VerifyLoginException("JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate());
}
if(isset($claims->identityPublicKey)){
@ -188,7 +192,7 @@ class ProcessLoginTask extends AsyncTask{
public function onCompletion() : void{
/**
* @var \Closure $callback
* @phpstan-var \Closure(bool, bool, ?string, ?string) : void $callback
* @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback
*/
$callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
$callback($this->authenticated, $this->authRequired, $this->error, $this->clientPublicKey);

View File

@ -23,6 +23,16 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
use pocketmine\lang\Translatable;
class VerifyLoginException extends \RuntimeException{
private Translatable|string $disconnectMessage;
public function __construct(string $message, Translatable|string|null $disconnectMessage = null, int $code = 0, ?\Throwable $previous = null){
parent::__construct($message, $code, $previous);
$this->disconnectMessage = $disconnectMessage ?? $message;
}
public function getDisconnectMessage() : Translatable|string{ return $this->disconnectMessage; }
}