diff --git a/plugin.yml b/plugin.yml new file mode 100644 index 0000000..ce3a323 --- /dev/null +++ b/plugin.yml @@ -0,0 +1,9 @@ +name: DiscordVerify +author: alvin0319 +main: TeamBixby\DiscordVerify\DiscordVerify +version: 1.0.0 +api: 3.0.0 + +permissions: + discordverify.command.use: + default: true \ No newline at end of file diff --git a/resources/config.yml b/resources/config.yml new file mode 100644 index 0000000..d4dd189 --- /dev/null +++ b/resources/config.yml @@ -0,0 +1,5 @@ +address: ~ +port: ~ +password: ~ + +bindPort: ~ \ No newline at end of file diff --git a/src/TeamBixby/DiscordVerify/DiscordThread.php b/src/TeamBixby/DiscordVerify/DiscordThread.php new file mode 100644 index 0000000..7a2f2c1 --- /dev/null +++ b/src/TeamBixby/DiscordVerify/DiscordThread.php @@ -0,0 +1,160 @@ +classLoader = $loader; + $this->targetHost = $targetHost; + $this->targetPort = $targetPort; + $this->bindPort = $bindPort; + $this->password = $password; + $this->inboundQueue = $inboundQueue; + $this->outboundQueue = $outboundQueue; + } + + /** + * @throws DiscordException + */ + public function run() : void{ + $this->classLoader->register(); + + $this->shutdown = false; + + $sendSock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + $recvSock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + + socket_set_nonblock($recvSock); + + if($sendSock === false || $recvSock === false){ + throw DiscordException::wrap("Failed to create socket"); + } + + if(@socket_bind($recvSock, "0.0.0.0", $this->bindPort) === false){ + throw DiscordException::wrap("Failed to bind port"); + } + + socket_set_nonblock($recvSock); + + while(!$this->shutdown){ + $this->receiveData($recvSock); + $this->sendData($sendSock); + } + socket_close($recvSock); + socket_close($sendSock); + } + + /** + * @param resource $recvSock + * + * @throws DiscordException + */ + private function receiveData($recvSock) : void{ + $buffer = ""; + if(@socket_recvfrom($recvSock, $buffer, 65535, 0, $source, $port) === false){ + $errno = socket_last_error($recvSock); + if($errno === SOCKET_EWOULDBLOCK){ + return; + } + throw new DiscordException("Failed to recv (errno $errno): " . trim(socket_strerror($errno)), $errno); + //return; + } + if($buffer !== null && $buffer !== ""){ + $data = json_decode($buffer, true); + if(!is_array($data)){ + throw DiscordException::wrap("Expected array, got " . (is_object($data) ? get_class($data) : gettype($data))); + } + $password = $data["password"] ?? null; + if(!is_string($password)){ + MainLogger::getLogger()->notice("Bad socket received from $source:$port"); + MainLogger::getLogger()->notice("Password does not exist on response body"); + return; + } + if($password !== $this->password){ + MainLogger::getLogger()->notice("Bad socket received from $source:$port"); + MainLogger::getLogger()->notice("Password does not match"); + return; + } + $action = $data["action"]; + $actionData = $data["data"]; + + $this->outboundQueue[] = [ + self::KEY_ACTION => $action, + self::KEY_DATA => $actionData + ]; + } + } + + /** + * @param resource $sendSock + * + * @throws DiscordException + */ + private function sendData($sendSock) : void{ + while($this->inboundQueue->count() > 0){ + $chunk = $this->inboundQueue->shift(); + $data = [ + "data" => $chunk, + "password" => $this->password + ]; + if(@socket_sendto($sendSock, json_encode($data), PHP_INT_MAX, 0, $this->targetHost, $this->targetPort) === false){ + $errno = socket_last_error($sendSock); + if($errno === SOCKET_EWOULDBLOCK){ + return; + } + throw DiscordException::wrap("Failed to send socket: " . trim(socket_strerror($errno))); + } + } + } + + public function shutdown() : void{ + $this->shutdown = true; + } +} \ No newline at end of file diff --git a/src/TeamBixby/DiscordVerify/DiscordVerify.php b/src/TeamBixby/DiscordVerify/DiscordVerify.php new file mode 100644 index 0000000..ba41f71 --- /dev/null +++ b/src/TeamBixby/DiscordVerify/DiscordVerify.php @@ -0,0 +1,116 @@ +saveDefaultConfig(); + $config = $this->getConfig(); + + if($config->get("address", null) === null || $config->get("port", null) === null || $config->get("password", null) === null || $config->get("bindPort", null) === null){ + $this->getLogger()->emergency("Please fill the config correctly before use this plugin!"); + $this->getServer()->getPluginManager()->disablePlugin($this); + return; + } + + $this->inboundQueue = new Volatile(); + $this->outboundQueue = new Volatile(); + + try{ + $this->thread = new DiscordThread($this->getServer()->getLoader(), $config->get("address"), $config->get("port"), $config->get("bindPort"), $config->get("password"), $this->inboundQueue, $this->outboundQueue); + $this->thread->start(PTHREADS_INHERIT_ALL); + $this->canThreadClose = true; + }catch(DiscordException $e){ + $this->getLogger()->emergency("Failed to run DiscordThread"); + $this->getLogger()->logException($e); + $this->canThreadClose = false; + $this->getServer()->getPluginManager()->disablePlugin($this); + return; + } + + if(file_exists($file = $this->getDataFolder() . "verified_players.json")){ + $this->data = json_decode(file_get_contents($file), true); + } + + $this->getScheduler()->scheduleRepeatingTask(new ClosureTask(function(int $unused) : void{ + while($this->outboundQueue->count() > 0){ + $chunk = $this->outboundQueue->shift(); + $data = $chunk["data"]; + switch($chunk["action"]){ + case self::ACTION_VERIFIED: + $player = $data["player"]; + $this->data[strtolower($player)] = $data["discordId"]; + if(($player = $this->getServer()->getPlayerExact($player)) !== null){ + $player->sendMessage(TextFormat::GREEN . "You've verified!"); + } + break; + case self::ACTION_UNVERIFIED: + $player = $data["player"]; + if(isset($this->data[strtolower($player)])){ + unset($this->data[strtolower($player)]); + if(($player = $this->getServer()->getPlayerExact($player)) !== null){ + $player->sendMessage(TextFormat::GREEN . "You've unverified!"); + } + } + break; + } + } + }), 5); + + $this->getServer()->getCommandMap()->register("discordverify", new VerifyCommand()); + } + + public function onDisable() : void{ + if($this->canThreadClose){ + $this->thread->shutdown(); + while($this->thread->isRunning()){ + // SAFETY THREAD CLOSE + } + } + file_put_contents($this->getDataFolder() . "verified_players.json", json_encode($this->data)); + } + + public function addQueue(array $data) : void{ + $this->inboundQueue[] = $data; + } + + public function isVerified(string $player) : bool{ + return isset($this->data[strtolower($player)]); + } +} \ No newline at end of file diff --git a/src/TeamBixby/DiscordVerify/command/VerifyCommand.php b/src/TeamBixby/DiscordVerify/command/VerifyCommand.php new file mode 100644 index 0000000..01c8af6 --- /dev/null +++ b/src/TeamBixby/DiscordVerify/command/VerifyCommand.php @@ -0,0 +1,41 @@ +setPermission("discordverify.command.use"); + } + + public function execute(CommandSender $sender, string $commandLabel, array $args) : bool{ + if(!$this->testPermission($sender)){ + return false; + } + if(!$sender instanceof Player){ + $sender->sendMessage(TextFormat::RED . "You can't use this command on console."); + return false; + } + if(DiscordVerify::getInstance()->isVerified($sender->getName())){ + $sender->sendMessage(TextFormat::RED . "You are already verified."); + return false; + } + DiscordVerify::getInstance()->addQueue([ + "player" => $sender->getName(), + "random_token" => $token = substr(UUID::fromRandom()->toString(), 0, 6) + ]); + $sender->sendMessage(TextFormat::GREEN . "Use the verify command on Discord server using " . $token); + return true; + } +} \ No newline at end of file diff --git a/src/TeamBixby/DiscordVerify/util/DiscordException.php b/src/TeamBixby/DiscordVerify/util/DiscordException.php new file mode 100644 index 0000000..483d909 --- /dev/null +++ b/src/TeamBixby/DiscordVerify/util/DiscordException.php @@ -0,0 +1,14 @@ +