Skip to content
This repository was archived by the owner on Apr 27, 2025. It is now read-only.

Commit 91b7e95

Browse files
Add diagnosis to /pv
1 parent f1f84dc commit 91b7e95

File tree

4 files changed

+155
-37
lines changed

4 files changed

+155
-37
lines changed

src/ProjectInfinity/PocketVote/cmd/PocketVoteCommand.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use pocketmine\utils\TextFormat;
99
use ProjectInfinity\PocketVote\PocketVote;
1010
use ProjectInfinity\PocketVote\task\guru\SetLinkNameTask;
11+
use ProjectInfinity\PocketVote\task\DiagnoseTask;
1112

1213
class PocketVoteCommand extends Command implements PluginIdentifiableCommand {
1314

@@ -24,7 +25,7 @@ public function execute(CommandSender $sender, $commandLabel, array $args) {
2425
return true;
2526
}
2627
if(count($args) === 0) {
27-
$sender->sendMessage(TextFormat::AQUA.'Specify an action: SECRET, IDENTITY, CMD, CMDO');
28+
$sender->sendMessage(TextFormat::AQUA.'Specify an action: SECRET, IDENTITY, DIAGNOSE, CMD, CMDO, LINK');
2829
return true;
2930
}
3031
switch(strtoupper($args[0])) {
@@ -59,6 +60,11 @@ public function execute(CommandSender $sender, $commandLabel, array $args) {
5960
$this->plugin->saveConfig();
6061
break;
6162

63+
case 'DIAGNOSE':
64+
$sender->sendMessage(TextFormat::GREEN.'Scheduling a diagnosis...');
65+
$this->plugin->getServer()->getScheduler()->scheduleAsyncTask(new DiagnoseTask($this->plugin->getDescription()->getVersion(), $sender->getName()));
66+
break;
67+
6268
case 'CMD':
6369
if(count($args) < 2) {
6470
$sender->sendMessage(TextFormat::RED.'You need to specify a command. Variables: %player, %ip, %site');
@@ -176,7 +182,7 @@ public function execute(CommandSender $sender, $commandLabel, array $args) {
176182
break;
177183

178184
default:
179-
$sender->sendMessage(TextFormat::RED.'Invalid option. Specify SECRET, IDENTITY, CMD or CMDO.');
185+
$sender->sendMessage(TextFormat::RED.'Invalid option. Specify SECRET, IDENTITY, DIAGNOSE, CMD, CMDO or LINK.');
180186
}
181187
return true;
182188
}

src/ProjectInfinity/PocketVote/lib/Firebase/JWT.php

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ class JWT
2929
*/
3030
public static $leeway = 0;
3131

32+
/**
33+
* Allow the current timestamp to be specified.
34+
* Useful for fixing a value within unit testing.
35+
*
36+
* Will default to PHP time() value if null.
37+
*/
38+
public static $timestamp = null;
39+
3240
public static $supported_algs = array(
3341
'HS256' => array('hash_hmac', 'SHA256'),
3442
'HS512' => array('hash_hmac', 'SHA512'),
@@ -39,15 +47,14 @@ class JWT
3947
/**
4048
* Decodes a JWT string into a PHP object.
4149
*
42-
* @param string $jwt The JWT
43-
* @param string|array|null $key The key, or map of keys.
44-
* If the algorithm used is asymmetric, this is the public key
45-
* @param array $allowed_algs List of supported verification algorithms
46-
* Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
50+
* @param string $jwt The JWT
51+
* @param string|array $key The key, or map of keys.
52+
* If the algorithm used is asymmetric, this is the public key
53+
* @param array $allowed_algs List of supported verification algorithms
54+
* Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
4755
*
4856
* @return object The JWT's payload as a PHP object
4957
*
50-
* @throws DomainException Algorithm was not provided
5158
* @throws UnexpectedValueException Provided JWT was invalid
5259
* @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed
5360
* @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'
@@ -59,47 +66,52 @@ class JWT
5966
*/
6067
public static function decode($jwt, $key, $allowed_algs = array())
6168
{
69+
$timestamp = is_null(static::$timestamp) ? time() : static::$timestamp;
70+
6271
if (empty($key)) {
6372
throw new InvalidArgumentException('Key may not be empty');
6473
}
74+
if (!is_array($allowed_algs)) {
75+
throw new InvalidArgumentException('Algorithm not allowed');
76+
}
6577
$tks = explode('.', $jwt);
6678
if (count($tks) != 3) {
6779
throw new UnexpectedValueException('Wrong number of segments');
6880
}
6981
list($headb64, $bodyb64, $cryptob64) = $tks;
70-
if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))) {
82+
if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
7183
throw new UnexpectedValueException('Invalid header encoding');
7284
}
73-
if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64))) {
85+
if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
7486
throw new UnexpectedValueException('Invalid claims encoding');
7587
}
76-
$sig = JWT::urlsafeB64Decode($cryptob64);
77-
88+
$sig = static::urlsafeB64Decode($cryptob64);
89+
7890
if (empty($header->alg)) {
79-
throw new DomainException('Empty algorithm');
91+
throw new UnexpectedValueException('Empty algorithm');
8092
}
81-
if (empty(self::$supported_algs[$header->alg])) {
82-
throw new DomainException('Algorithm not supported');
93+
if (empty(static::$supported_algs[$header->alg])) {
94+
throw new UnexpectedValueException('Algorithm not supported');
8395
}
84-
if (!is_array($allowed_algs) || !in_array($header->alg, $allowed_algs)) {
85-
throw new DomainException('Algorithm not allowed');
96+
if (!in_array($header->alg, $allowed_algs)) {
97+
throw new UnexpectedValueException('Algorithm not allowed');
8698
}
8799
if (is_array($key) || $key instanceof \ArrayAccess) {
88100
if (isset($header->kid)) {
89101
$key = $key[$header->kid];
90102
} else {
91-
throw new DomainException('"kid" empty, unable to lookup correct key');
103+
throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
92104
}
93105
}
94106

95107
// Check the signature
96-
if (!JWT::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {
108+
if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {
97109
throw new SignatureInvalidException('Signature verification failed');
98110
}
99111

100112
// Check if the nbf if it is defined. This is the time that the
101113
// token can actually be used. If it's not yet that time, abort.
102-
if (isset($payload->nbf) && $payload->nbf > (time() + self::$leeway)) {
114+
if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
103115
throw new BeforeValidException(
104116
'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf)
105117
);
@@ -108,14 +120,14 @@ public static function decode($jwt, $key, $allowed_algs = array())
108120
// Check that this token has been created before 'now'. This prevents
109121
// using tokens that have been created for later use (and haven't
110122
// correctly used the nbf claim).
111-
if (isset($payload->iat) && $payload->iat > (time() + self::$leeway)) {
123+
if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
112124
throw new BeforeValidException(
113125
'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat)
114126
);
115127
}
116128

117129
// Check if this token has expired.
118-
if (isset($payload->exp) && (time() - self::$leeway) >= $payload->exp) {
130+
if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
119131
throw new ExpiredException('Expired token');
120132
}
121133

@@ -130,6 +142,7 @@ public static function decode($jwt, $key, $allowed_algs = array())
130142
* If the algorithm used is asymmetric, this is the private key
131143
* @param string $alg The signing algorithm.
132144
* Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
145+
* @param mixed $keyId
133146
* @param array $head An array with header elements to attach
134147
*
135148
* @return string A signed JWT
@@ -147,12 +160,12 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he
147160
$header = array_merge($head, $header);
148161
}
149162
$segments = array();
150-
$segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header));
151-
$segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload));
163+
$segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
164+
$segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
152165
$signing_input = implode('.', $segments);
153166

154-
$signature = JWT::sign($signing_input, $key, $alg);
155-
$segments[] = JWT::urlsafeB64Encode($signature);
167+
$signature = static::sign($signing_input, $key, $alg);
168+
$segments[] = static::urlsafeB64Encode($signature);
156169

157170
return implode('.', $segments);
158171
}
@@ -171,10 +184,10 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he
171184
*/
172185
public static function sign($msg, $key, $alg = 'HS256')
173186
{
174-
if (empty(self::$supported_algs[$alg])) {
187+
if (empty(static::$supported_algs[$alg])) {
175188
throw new DomainException('Algorithm not supported');
176189
}
177-
list($function, $algorithm) = self::$supported_algs[$alg];
190+
list($function, $algorithm) = static::$supported_algs[$alg];
178191
switch($function) {
179192
case 'hash_hmac':
180193
return hash_hmac($algorithm, $msg, $key, true);
@@ -204,11 +217,11 @@ public static function sign($msg, $key, $alg = 'HS256')
204217
*/
205218
private static function verify($msg, $signature, $key, $alg)
206219
{
207-
if (empty(self::$supported_algs[$alg])) {
220+
if (empty(static::$supported_algs[$alg])) {
208221
throw new DomainException('Algorithm not supported');
209222
}
210223

211-
list($function, $algorithm) = self::$supported_algs[$alg];
224+
list($function, $algorithm) = static::$supported_algs[$alg];
212225
switch($function) {
213226
case 'openssl':
214227
$success = openssl_verify($msg, $signature, $key, $algorithm);
@@ -223,13 +236,13 @@ private static function verify($msg, $signature, $key, $alg)
223236
if (function_exists('hash_equals')) {
224237
return hash_equals($signature, $hash);
225238
}
226-
$len = min(self::safeStrlen($signature), self::safeStrlen($hash));
239+
$len = min(static::safeStrlen($signature), static::safeStrlen($hash));
227240

228241
$status = 0;
229242
for ($i = 0; $i < $len; $i++) {
230243
$status |= (ord($signature[$i]) ^ ord($hash[$i]));
231244
}
232-
$status |= (self::safeStrlen($signature) ^ self::safeStrlen($hash));
245+
$status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash));
233246

234247
return ($status === 0);
235248
}
@@ -263,7 +276,7 @@ public static function jsonDecode($input)
263276
}
264277

265278
if (function_exists('json_last_error') && $errno = json_last_error()) {
266-
JWT::handleJsonError($errno);
279+
static::handleJsonError($errno);
267280
} elseif ($obj === null && $input !== 'null') {
268281
throw new DomainException('Null result with non-null input');
269282
}
@@ -283,7 +296,7 @@ public static function jsonEncode($input)
283296
{
284297
$json = json_encode($input);
285298
if (function_exists('json_last_error') && $errno = json_last_error()) {
286-
JWT::handleJsonError($errno);
299+
static::handleJsonError($errno);
287300
} elseif ($json === 'null' && $input !== null) {
288301
throw new DomainException('Null result with non-null input');
289302
}
@@ -335,8 +348,8 @@ private static function handleJsonError($errno)
335348
);
336349
throw new DomainException(
337350
isset($messages[$errno])
338-
? $messages[$errno]
339-
: 'Unknown JSON error: ' . $errno
351+
? $messages[$errno]
352+
: 'Unknown JSON error: ' . $errno
340353
);
341354
}
342355

@@ -354,4 +367,4 @@ private static function safeStrlen($str)
354367
}
355368
return strlen($str);
356369
}
357-
}
370+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
namespace ProjectInfinity\PocketVote\task;
4+
5+
use pocketmine\command\ConsoleCommandSender;
6+
use pocketmine\scheduler\AsyncTask;
7+
use pocketmine\Server;
8+
use pocketmine\utils\TextFormat;
9+
use ProjectInfinity\PocketVote\lib\Firebase\JWT;
10+
use ProjectInfinity\PocketVote\PocketVote;
11+
12+
class DiagnoseTask extends AsyncTask {
13+
public $identity;
14+
public $isDev;
15+
public $cert;
16+
public $secret;
17+
public $version;
18+
public $player;
19+
20+
public function __construct($version, $player) {
21+
$this->identity = PocketVote::getPlugin()->getConfig()->get('identity', null);
22+
$this->secret = PocketVote::getPlugin()->getConfig()->get('secret', null);
23+
$this->isDev = PocketVote::$dev;
24+
$this->cert = PocketVote::$cert;
25+
$this->version = $version;
26+
$this->player = $player;
27+
}
28+
29+
public function onRun() {
30+
31+
if($this->identity === null) return;
32+
33+
$curl = curl_init($this->isDev ? 'http://127.0.0.1/v2/diagnose' : 'https://api.pocketvote.io/v2/diagnose');
34+
35+
curl_setopt_array($curl, [
36+
CURLOPT_RETURNTRANSFER => 1,
37+
CURLOPT_PORT => $this->isDev ? 9000 : 443,
38+
CURLOPT_HEADER => false,
39+
CURLOPT_SSL_VERIFYPEER => true,
40+
CURLOPT_SSL_VERIFYHOST => 2,
41+
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
42+
CURLOPT_CAINFO => $this->cert,
43+
CURLOPT_USERAGENT => 'PocketVote v'.$this->version,
44+
CURLOPT_HTTPHEADER => ['Identity: '.$this->identity],
45+
]);
46+
47+
$res = curl_exec($curl);
48+
49+
if($res === false) {
50+
$this->setResult((object)['success' => false, 'error' => curl_error($curl)]);
51+
curl_close($curl);
52+
return;
53+
}
54+
55+
curl_close($curl);
56+
$this->setResult(json_decode($res));
57+
}
58+
59+
public function onCompletion(Server $server) {
60+
$player = $this->player === 'CONSOLE' ? new ConsoleCommandSender() : $server->getPlayer($this->player);
61+
if($player === null) return;
62+
63+
if(!$this->hasResult()) {
64+
$player->sendMessage(TextFormat::RED.'Failed to diagnose PocketVote. Try again later.');
65+
return;
66+
}
67+
68+
$result = $this->getResult();
69+
70+
if(!$result->success && isset($result->error)) {
71+
$player->sendMessage(TextFormat::RED.'An error occurred while contacting the PocketVote servers, please try again later.');
72+
$server->getLogger()->error('[PocketVote] curl error occurred during DiagnoseTask: '.$result->error);
73+
return;
74+
}
75+
76+
if(isset($result->payload)) {
77+
$player->sendMessage(($result->payload->foundServer ? TextFormat::GREEN.'' : TextFormat::RED.'').' Found server');
78+
$player->sendMessage(($result->payload->hasVotes ? TextFormat::GREEN.'' : TextFormat::RED.'').' Has votes (trivial)');
79+
80+
try {
81+
$token = JWT::decode($result->payload->voteSample, $this->secret, array('HS256'));
82+
if($token->player === 'PocketVoteSample' && $token->ip === '127.0.0.1' && $token->site === 'PocketVote.io') {
83+
$player->sendMessage((TextFormat::GREEN.'').' Decode sample vote');
84+
} else {
85+
throw new \ErrorException('Token did not meet expectations');
86+
}
87+
} catch(\Exception $e) {
88+
$player->sendMessage((TextFormat::RED.'').' Decode sample vote');
89+
$player->sendMessage(TextFormat::YELLOW.'Reason: '.$e->getMessage());
90+
}
91+
92+
$player->sendMessage(TextFormat::YELLOW.'A test vote will be dispatched momentarily. If more than a couple of minutes passes the dispatch has failed.');
93+
} else {
94+
$player->sendMessage(TextFormat::RED.'Please wait before trying to use this command again.');
95+
}
96+
}
97+
}

src/ProjectInfinity/PocketVote/task/TopVoterTask.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ public function onRun() {
3939

4040
if($res === false) {
4141
$this->setResult((object)['success' => false, 'error' => curl_error($curl)]);
42+
curl_close($curl);
4243
return;
4344
}
4445

46+
curl_close($curl);
4547
$this->setResult(json_decode($res));
4648
}
4749

0 commit comments

Comments
 (0)