diff --git a/README.md b/README.md index 4bf5389ae..d98247f9b 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ I would like to extend my sincere thanks to the following sponsors for helping f | [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. | | [**HostBend**](https://hostbend.com/) | HostBend offers a variety of solutions for developers, students, and others who have a tight budget but don't want to compromise quality and support. | | [**Capitol Hosting Solutions**](https://capitolsolutions.cloud/) | CHS is *the* budget friendly hosting company for Australian and American gamers, offering a variety of plans from Web Hosting to Game Servers; Custom Solutions too! | -| [**ByteAnia**](https://ByteAnia.com/) | ByteAnia offers the best performing and most affordable **Ryzen 5000 Series hosting** on the market for *unbeatable prices*! | +| [**ByteAnia**](https://byteania.com/?utm_source=pterodactyl) | ByteAnia offers the best performing and most affordable **Ryzen 5000 Series hosting** on the market for *unbeatable prices*! | ## Documentation * [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html) diff --git a/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php b/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php index a4ea4dd09..b7b819b44 100644 --- a/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php +++ b/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php @@ -6,4 +6,11 @@ use Pterodactyl\Exceptions\DisplayException; class TwoFactorAuthenticationTokenInvalid extends DisplayException { + /** + * TwoFactorAuthenticationTokenInvalid constructor. + */ + public function __construct() + { + parent::__construct('The provided two-factor authentication token was not valid.'); + } } diff --git a/app/Http/Controllers/Api/Client/Servers/BackupController.php b/app/Http/Controllers/Api/Client/Servers/BackupController.php index 9998a47a2..a228b38cb 100644 --- a/app/Http/Controllers/Api/Client/Servers/BackupController.php +++ b/app/Http/Controllers/Api/Client/Servers/BackupController.php @@ -195,7 +195,7 @@ class BackupController extends ClientApiController // actions against it via the Panel API. $server->update(['status' => Server::STATUS_RESTORING_BACKUP]); - $this->repository->setServer($server)->restore($backup, $url ?? null, $request->input('truncate') === 'true'); + $this->repository->setServer($server)->restore($backup, $url ?? null, $request->input('truncate')); }); return $this->returnNoContent(); diff --git a/app/Http/Controllers/Api/Client/Servers/ResourceUtilizationController.php b/app/Http/Controllers/Api/Client/Servers/ResourceUtilizationController.php index 618f39548..333b24a9b 100644 --- a/app/Http/Controllers/Api/Client/Servers/ResourceUtilizationController.php +++ b/app/Http/Controllers/Api/Client/Servers/ResourceUtilizationController.php @@ -2,7 +2,9 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers; +use Carbon\Carbon; use Pterodactyl\Models\Server; +use Illuminate\Cache\Repository; use Pterodactyl\Transformers\Api\Client\StatsTransformer; use Pterodactyl\Repositories\Wings\DaemonServerRepository; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; @@ -10,27 +12,35 @@ use Pterodactyl\Http\Requests\Api\Client\Servers\GetServerRequest; class ResourceUtilizationController extends ClientApiController { + private Repository $cache; private DaemonServerRepository $repository; /** * ResourceUtilizationController constructor. */ - public function __construct(DaemonServerRepository $repository) + public function __construct(Repository $cache, DaemonServerRepository $repository) { parent::__construct(); + $this->cache = $cache; $this->repository = $repository; } /** - * Return the current resource utilization for a server. + * Return the current resource utilization for a server. This value is cached for up to + * 20 seconds at a time to ensure that repeated requests to this endpoint do not cause + * a flood of unnecessary API calls. * * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function __invoke(GetServerRequest $request, Server $server): array { - $stats = $this->repository->setServer($server)->getDetails(); + $stats = $this->cache + ->tags(['resources']) + ->remember($server->uuid, Carbon::now()->addSeconds(20), function () use ($server) { + return $this->repository->setServer($server)->getDetails(); + }); return $this->fractal->item($stats) ->transformWith($this->getTransformer(StatsTransformer::class)) diff --git a/app/Http/Controllers/Api/Client/TwoFactorController.php b/app/Http/Controllers/Api/Client/TwoFactorController.php index bc5533921..ec3590d25 100644 --- a/app/Http/Controllers/Api/Client/TwoFactorController.php +++ b/app/Http/Controllers/Api/Client/TwoFactorController.php @@ -57,7 +57,14 @@ class TwoFactorController extends ClientApiController /** * Updates a user's account to have two-factor enabled. * + * @return \Illuminate\Http\JsonResponse + * * @throws \Throwable + * @throws \Illuminate\Validation\ValidationException + * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException + * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException + * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException + * @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid */ public function store(Request $request): JsonResponse { diff --git a/app/Models/Schedule.php b/app/Models/Schedule.php index 432f4d5f8..240873cac 100644 --- a/app/Models/Schedule.php +++ b/app/Models/Schedule.php @@ -122,13 +122,14 @@ class Schedule extends Model * Returns the schedule's execution crontab entry as a string. * * @return \Carbon\CarbonImmutable + * @throws \Exception */ public function getNextRunDate() { $formatted = sprintf('%s %s %s %s %s', $this->cron_minute, $this->cron_hour, $this->cron_day_of_month, $this->cron_month, $this->cron_day_of_week); return CarbonImmutable::createFromTimestamp( - CronExpression::factory($formatted)->getNextRunDate()->getTimestamp() + (new CronExpression($formatted))->getNextRunDate()->getTimestamp() ); } diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index 6a96075c5..9a8c1c3c4 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -5,10 +5,12 @@ namespace Pterodactyl\Services\Servers; use Illuminate\Support\Arr; use Pterodactyl\Models\Server; use Pterodactyl\Models\Allocation; +use Illuminate\Support\Facades\Log; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Exceptions\DisplayException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Pterodactyl\Repositories\Wings\DaemonServerRepository; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; class BuildModificationService { @@ -78,10 +80,18 @@ class BuildModificationService $updateData = $this->structureService->handle($server); + // Because Wings always fetches an updated configuration from the Panel when booting + // a server this type of exception can be safely "ignored" and just written to the logs. + // Ideally this request succeedes so we can apply resource modifications on the fly + // but if it fails it isn't the end of the world. if (!empty($updateData['build'])) { - $this->daemonServerRepository->setServer($server)->update([ - 'build' => $updateData['build'], - ]); + try { + $this->daemonServerRepository->setServer($server)->update([ + 'build' => $updateData['build'], + ]); + } catch (DaemonConnectionException $exception) { + Log::warning($exception, ['server_id' => $server->id]); + } } $this->connection->commit(); diff --git a/app/Services/Users/ToggleTwoFactorService.php b/app/Services/Users/ToggleTwoFactorService.php index 908fc35d6..28faff7f1 100644 --- a/app/Services/Users/ToggleTwoFactorService.php +++ b/app/Services/Users/ToggleTwoFactorService.php @@ -74,7 +74,7 @@ class ToggleTwoFactorService $isValidToken = $this->google2FA->verifyKey($secret, $token, config()->get('pterodactyl.auth.2fa.window')); if (!$isValidToken) { - throw new TwoFactorAuthenticationTokenInvalid('The token provided is not valid.'); + throw new TwoFactorAuthenticationTokenInvalid(); } return $this->connection->transaction(function () use ($user, $toggleState) { @@ -94,6 +94,9 @@ class ToggleTwoFactorService $inserts[] = [ 'user_id' => $user->id, 'token' => password_hash($token, PASSWORD_DEFAULT), + // insert() won't actually set the time on the models, so make sure we do this + // manually here. + 'created_at' => Carbon::now(), ]; $tokens[] = $token; diff --git a/database/Seeders/eggs/minecraft/egg-bungeecord.json b/database/Seeders/eggs/minecraft/egg-bungeecord.json index e768c78d2..23103f54d 100644 --- a/database/Seeders/eggs/minecraft/egg-bungeecord.json +++ b/database/Seeders/eggs/minecraft/egg-bungeecord.json @@ -3,7 +3,7 @@ "meta": { "version": "PTDL_v1" }, - "exported_at": "2020-11-03T04:22:56+00:00", + "exported_at": "2021-03-21T17:52:00+00:00", "name": "Bungeecord", "author": "support@pterodactyl.io", "description": "For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. Whether you are a small server wishing to string multiple game-modes together, or the owner of the ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be able to unlock your community's full potential.", @@ -11,7 +11,7 @@ "images": ["quay.io\/pterodactyl\/core:java", "quay.io\/pterodactyl\/core:java-11"], "startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}", "config": { - "files": "{\r\n \"config.yml\": {\r\n \"parser\": \"yaml\",\r\n \"find\": {\r\n \"listeners[0].query_enabled\": true,\r\n \"listeners[0].query_port\": \"{{server.build.default.port}}\",\r\n \"listeners[0].host\": \"0.0.0.0:{{server.build.default.port}}\",\r\n \"servers.*.address\": {\r\n \"regex:^(127\\\\.0\\\\.0\\\\.1|localhost)(:\\\\d{1,5})?$\": \"{{config.docker.interface}}$2\"\r\n }\r\n }\r\n }\r\n}", + "files": "{\r\n \"config.yml\": {\r\n \"parser\": \"yaml\",\r\n \"find\": {\r\n \"listeners[0].query_port\": \"{{server.build.default.port}}\",\r\n \"listeners[0].host\": \"0.0.0.0:{{server.build.default.port}}\",\r\n \"servers.*.address\": {\r\n \"regex:^(127\\\\.0\\\\.0\\\\.1|localhost)(:\\\\d{1,5})?$\": \"{{config.docker.interface}}$2\"\r\n }\r\n }\r\n }\r\n}", "startup": "{\r\n \"done\": \"Listening on \",\r\n \"userInteraction\": [\r\n \"Listening on \/0.0.0.0:25577\"\r\n ]\r\n}", "logs": "{\r\n \"custom\": false,\r\n \"location\": \"proxy.log.0\"\r\n}", "stop": "end" diff --git a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json b/database/Seeders/eggs/minecraft/egg-forge-minecraft.json index 986602c19..3793d7e55 100644 --- a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json +++ b/database/Seeders/eggs/minecraft/egg-forge-minecraft.json @@ -4,7 +4,7 @@ "version": "PTDL_v1", "update_url": null }, - "exported_at": "2021-02-22T19:08:49+04:00", + "exported_at": "2021-03-15T18:04:38+02:00", "name": "Forge Minecraft", "author": "support@pterodactyl.io", "description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.", @@ -15,6 +15,7 @@ "quay.io\/pterodactyl\/core:java", "quay.io\/pterodactyl\/core:java-11" ], + "file_denylist": [], "startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}", "config": { "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"enable-query\": \"true\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}", @@ -24,7 +25,7 @@ }, "scripts": { "installation": { - "script": "#!\/bin\/bash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl jq\r\n\r\n#Go into main direction\r\nif [ ! -d \/mnt\/server ]; then\r\n mkdir \/mnt\/server\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\nif [ ! -z ${FORGE_VERSION} ]; then\r\n DOWNLOAD_LINK=https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/${FORGE_VERSION}\/forge-${FORGE_VERSION}\r\n FORGE_JAR=forge-${FORGE_VERSION}.jar\r\nelse\r\n JSON_DATA=$(curl -sSL https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/promotions_slim.json)\r\n\r\n if [ \"${MC_VERSION}\" == \"latest\" ] || [ \"${MC_VERSION}\" == \"\" ] ; then\r\n echo -e \"getting latest recommended version of forge.\"\r\n MC_VERSION=$(echo -e ${JSON_DATA} | jq -r '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains(\"recommended\")) | split(\"-\")[0]' | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -1)\r\n \tBUILD_TYPE=recommended\r\n fi\r\n\r\n if [ \"${BUILD_TYPE}\" != \"recommended\" ] && [ \"${BUILD_TYPE}\" != \"latest\" ]; then\r\n BUILD_TYPE=recommended\r\n fi\r\n\r\n echo -e \"minecraft version: ${MC_VERSION}\"\r\n echo -e \"build type: ${BUILD_TYPE}\"\r\n\r\n ## some variables for getting versions and things\r\n FILE_SITE=$(echo -e ${JSON_DATA} | jq -r '.homepage' | sed \"s\/http:\/https:\/g\")\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" --arg BUILD_TYPE \"${BUILD_TYPE}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains($BUILD_TYPE))')\r\n\r\n ## locating the forge version\r\n if [ \"${VERSION_KEY}\" == \"\" ] && [ \"${BUILD_TYPE}\" == \"recommended\" ]; then\r\n echo -e \"dropping back to latest from recommended due to there not being a recommended version of forge for the mc version requested.\"\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains(\"recommended\"))')\r\n fi\r\n\r\n ## Error if the mc version set wasn't valid.\r\n if [ \"${VERSION_KEY}\" == \"\" ] || [ \"${VERSION_KEY}\" == \"null\" ]; then\r\n \techo -e \"The install failed because there is no valid version of forge for the version on minecraft selected.\"\r\n \texit 1\r\n fi\r\n\r\n FORGE_VERSION=$(echo -e ${JSON_DATA} | jq -r --arg VERSION_KEY \"$VERSION_KEY\" '.promos | .[$VERSION_KEY]')\r\n\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ] || [ \"${MC_VERSION}\" == \"1.8.9\" ]; then\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}.jar\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ]; then\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}-universal.jar\r\n fi\r\n else\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}.jar\r\n fi\r\nfi\r\n\r\n\r\n#Adding .jar when not eding by SERVER_JARFILE\r\nif [[ ! $SERVER_JARFILE = *\\.jar ]]; then\r\n SERVER_JARFILE=\"$SERVER_JARFILE.jar\"\r\nfi\r\n\r\n#Downloading jars\r\necho -e \"Downloading forge version ${FORGE_VERSION}\"\r\necho -e \"Download link is ${DOWNLOAD_LINK}\"\r\nif [ ! -z \"${DOWNLOAD_LINK}\" ]; then \r\n if curl --output \/dev\/null --silent --head --fail ${DOWNLOAD_LINK}-installer.jar; then\r\n echo -e \"installer jar download link is valid.\"\r\n else\r\n echo -e \"link is invalid closing out\"\r\n exit 2\r\n fi\r\nelse\r\n echo -e \"no download link closing out\"\r\n exit 3\r\nfi\r\n\r\ncurl -s -o installer.jar -sS ${DOWNLOAD_LINK}-installer.jar\r\n\r\n#Checking if downloaded jars exist\r\nif [ ! -f .\/installer.jar ]; then\r\n echo \"!!! Error by downloading forge version ${FORGE_VERSION} !!!\"\r\n exit\r\nfi\r\n\r\n#Installing server\r\necho -e \"Installing forge server.\\n\"\r\njava -jar installer.jar --installServer || { echo -e \"install failed\"; exit 4; }\r\n\r\nmv $FORGE_JAR $SERVER_JARFILE\r\n\r\n#Deleting installer.jar\r\necho -e \"Deleting installer.jar file.\\n\"\r\nrm -rf installer.jar", + "script": "#!\/bin\/bash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl jq\r\n\r\n#Go into main direction\r\nif [ ! -d \/mnt\/server ]; then\r\n mkdir \/mnt\/server\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\nif [ ! -z ${FORGE_VERSION} ]; then\r\n DOWNLOAD_LINK=https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/${FORGE_VERSION}\/forge-${FORGE_VERSION}\r\n FORGE_JAR=forge-${FORGE_VERSION}*.jar\r\nelse\r\n JSON_DATA=$(curl -sSL https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/promotions_slim.json)\r\n\r\n if [ \"${MC_VERSION}\" == \"latest\" ] || [ \"${MC_VERSION}\" == \"\" ] ; then\r\n echo -e \"getting latest recommended version of forge.\"\r\n MC_VERSION=$(echo -e ${JSON_DATA} | jq -r '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains(\"recommended\")) | split(\"-\")[0]' | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -1)\r\n \tBUILD_TYPE=recommended\r\n fi\r\n\r\n if [ \"${BUILD_TYPE}\" != \"recommended\" ] && [ \"${BUILD_TYPE}\" != \"latest\" ]; then\r\n BUILD_TYPE=recommended\r\n fi\r\n\r\n echo -e \"minecraft version: ${MC_VERSION}\"\r\n echo -e \"build type: ${BUILD_TYPE}\"\r\n\r\n ## some variables for getting versions and things\r\n FILE_SITE=$(echo -e ${JSON_DATA} | jq -r '.homepage' | sed \"s\/http:\/https:\/g\")\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" --arg BUILD_TYPE \"${BUILD_TYPE}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains($BUILD_TYPE))')\r\n\r\n ## locating the forge version\r\n if [ \"${VERSION_KEY}\" == \"\" ] && [ \"${BUILD_TYPE}\" == \"recommended\" ]; then\r\n echo -e \"dropping back to latest from recommended due to there not being a recommended version of forge for the mc version requested.\"\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains(\"recommended\"))')\r\n fi\r\n\r\n ## Error if the mc version set wasn't valid.\r\n if [ \"${VERSION_KEY}\" == \"\" ] || [ \"${VERSION_KEY}\" == \"null\" ]; then\r\n \techo -e \"The install failed because there is no valid version of forge for the version on minecraft selected.\"\r\n \texit 1\r\n fi\r\n\r\n FORGE_VERSION=$(echo -e ${JSON_DATA} | jq -r --arg VERSION_KEY \"$VERSION_KEY\" '.promos | .[$VERSION_KEY]')\r\n\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ] || [ \"${MC_VERSION}\" == \"1.8.9\" ]; then\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}.jar\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ]; then\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}-universal.jar\r\n fi\r\n else\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}.jar\r\n fi\r\nfi\r\n\r\n\r\n#Adding .jar when not eding by SERVER_JARFILE\r\nif [[ ! $SERVER_JARFILE = *\\.jar ]]; then\r\n SERVER_JARFILE=\"$SERVER_JARFILE.jar\"\r\nfi\r\n\r\n#Downloading jars\r\necho -e \"Downloading forge version ${FORGE_VERSION}\"\r\necho -e \"Download link is ${DOWNLOAD_LINK}\"\r\nif [ ! -z \"${DOWNLOAD_LINK}\" ]; then \r\n if curl --output \/dev\/null --silent --head --fail ${DOWNLOAD_LINK}-installer.jar; then\r\n echo -e \"installer jar download link is valid.\"\r\n else\r\n echo -e \"link is invalid closing out\"\r\n exit 2\r\n fi\r\nelse\r\n echo -e \"no download link closing out\"\r\n exit 3\r\nfi\r\n\r\ncurl -s -o installer.jar -sS ${DOWNLOAD_LINK}-installer.jar\r\n\r\n#Checking if downloaded jars exist\r\nif [ ! -f .\/installer.jar ]; then\r\n echo \"!!! Error by downloading forge version ${FORGE_VERSION} !!!\"\r\n exit\r\nfi\r\n\r\n#Installing server\r\necho -e \"Installing forge server.\\n\"\r\njava -jar installer.jar --installServer || { echo -e \"install failed\"; exit 4; }\r\n\r\nmv $FORGE_JAR $SERVER_JARFILE\r\n\r\n#Deleting installer.jar\r\necho -e \"Deleting installer.jar file.\\n\"\r\nrm -rf installer.jar", "container": "openjdk:8-jdk-slim", "entrypoint": "bash" } diff --git a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json index cba10959e..3577816e9 100644 --- a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json +++ b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json @@ -38,7 +38,7 @@ }, { "name": "Server Version", - "description": "The version of Minecraft Vanilla to install. Use \"latest\" to install the latest version, or use \"snapshot\" to install the latest snapshot.", + "description": "The version of Minecraft Vanilla to install. Use \"latest\" to install the latest version, or use \"snapshot\" to install the latest snapshot. Go to Settings > Reinstall Server to apply.", "env_variable": "VANILLA_VERSION", "default_value": "latest", "user_viewable": true, diff --git a/database/migrations/2021_03_21_104718_force_cron_month_field_to_have_value_if_missing.php b/database/migrations/2021_03_21_104718_force_cron_month_field_to_have_value_if_missing.php new file mode 100644 index 000000000..63448c220 --- /dev/null +++ b/database/migrations/2021_03_21_104718_force_cron_month_field_to_have_value_if_missing.php @@ -0,0 +1,31 @@ + => { - await http.post(`/api/client/servers/${uuid}/backups/${backup}/restore`); +export const restoreServerBackup = async (uuid: string, backup: string, truncate?: boolean): Promise => { + await http.post(`/api/client/servers/${uuid}/backups/${backup}/restore`, { + truncate, + }); }; diff --git a/resources/scripts/components/dashboard/AccountOverviewContainer.tsx b/resources/scripts/components/dashboard/AccountOverviewContainer.tsx index 9fc4e9719..1baf6b253 100644 --- a/resources/scripts/components/dashboard/AccountOverviewContainer.tsx +++ b/resources/scripts/components/dashboard/AccountOverviewContainer.tsx @@ -27,7 +27,7 @@ const Container = styled.div` `; export default () => { - const state = useLocation<{ twoFactorRedirect: boolean }>().state; + const { state } = useLocation(); return ( diff --git a/resources/scripts/components/dashboard/DashboardContainer.tsx b/resources/scripts/components/dashboard/DashboardContainer.tsx index 72357fd65..c04ca0820 100644 --- a/resources/scripts/components/dashboard/DashboardContainer.tsx +++ b/resources/scripts/components/dashboard/DashboardContainer.tsx @@ -12,10 +12,14 @@ import tw from 'twin.macro'; import useSWR from 'swr'; import { PaginatedResult } from '@/api/http'; import Pagination from '@/components/elements/Pagination'; +import { useLocation } from 'react-router-dom'; export default () => { + const { search } = useLocation(); + const defaultPage = Number(new URLSearchParams(search).get('page') || '1'); + + const [ page, setPage ] = useState((!isNaN(defaultPage) && defaultPage > 0) ? defaultPage : 1); const { clearFlashes, clearAndAddHttpError } = useFlash(); - const [ page, setPage ] = useState(1); const uuid = useStoreState(state => state.user.data!.uuid); const rootAdmin = useStoreState(state => state.user.data!.rootAdmin); const [ showOnlyAdmin, setShowOnlyAdmin ] = usePersistedState(`${uuid}:show_all_servers`, false); @@ -25,6 +29,20 @@ export default () => { () => getServers({ page, type: showOnlyAdmin ? 'admin' : undefined }), ); + useEffect(() => { + if (!servers) return; + if (servers.pagination.currentPage > 1 && !servers.items.length) { + setPage(1); + } + }, [ servers?.pagination.currentPage ]); + + useEffect(() => { + // Don't use react-router to handle changing this part of the URL, otherwise it + // triggers a needless re-render. We just want to track this in the URL incase the + // user refreshes the page. + window.history.replaceState(null, document.title, `/${page <= 1 ? '' : `?page=${page}`}`); + }, [ page ]); + useEffect(() => { if (error) clearAndAddHttpError({ key: 'dashboard', error }); if (!error) clearFlashes('dashboard'); diff --git a/resources/scripts/components/dashboard/ServerRow.tsx b/resources/scripts/components/dashboard/ServerRow.tsx index 40eebefac..2b33dbab7 100644 --- a/resources/scripts/components/dashboard/ServerRow.tsx +++ b/resources/scripts/components/dashboard/ServerRow.tsx @@ -59,7 +59,7 @@ export default ({ server, className }: { server: Server; className?: string }) = getStats().then(() => { // @ts-ignore - interval.current = setInterval(() => getStats(), 20000); + interval.current = setInterval(() => getStats(), 30000); }); return () => { diff --git a/resources/scripts/components/dashboard/forms/SetupTwoFactorModal.tsx b/resources/scripts/components/dashboard/forms/SetupTwoFactorModal.tsx index 57eefa680..c1a1db634 100644 --- a/resources/scripts/components/dashboard/forms/SetupTwoFactorModal.tsx +++ b/resources/scripts/components/dashboard/forms/SetupTwoFactorModal.tsx @@ -78,7 +78,7 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {

Two-factor authentication enabled

Two-factor authentication has been enabled on your account. Should you loose access to - this device you'll need to use on of the codes displayed below in order to access your + this device you'll need to use one of the codes displayed below in order to access your account.

diff --git a/resources/scripts/components/server/Console.tsx b/resources/scripts/components/server/Console.tsx index 4aedfb01e..77493107f 100644 --- a/resources/scripts/components/server/Console.tsx +++ b/resources/scripts/components/server/Console.tsx @@ -146,10 +146,10 @@ export default () => { // Add support for capturing keys terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => { - if (e.metaKey && e.key === 'c') { + if ((e.ctrlKey || e.metaKey) && e.key === 'c') { document.execCommand('copy'); return false; - } else if (e.metaKey && e.key === 'f') { + } else if ((e.ctrlKey || e.metaKey) && e.key === 'f') { e.preventDefault(); searchBar.show(); return false; diff --git a/resources/scripts/components/server/backups/BackupContextMenu.tsx b/resources/scripts/components/server/backups/BackupContextMenu.tsx index bef067319..06f101ba6 100644 --- a/resources/scripts/components/server/backups/BackupContextMenu.tsx +++ b/resources/scripts/components/server/backups/BackupContextMenu.tsx @@ -25,6 +25,7 @@ export default ({ backup }: Props) => { const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState); const [ modal, setModal ] = useState(''); const [ loading, setLoading ] = useState(false); + const [ truncate, setTruncate ] = useState(false); const { clearFlashes, clearAndAddHttpError } = useFlash(); const { mutate } = getServerBackups(); @@ -62,7 +63,7 @@ export default ({ backup }: Props) => { const doRestorationAction = () => { setLoading(true); clearFlashes('backups'); - restoreServerBackup(uuid, backup.uuid) + restoreServerBackup(uuid, backup.uuid, truncate) .then(() => setServerFromState(s => ({ ...s, status: 'restoring_backup', @@ -108,6 +109,8 @@ export default ({ backup }: Props) => { css={tw`text-red-500! w-5! h-5! mr-2`} id={'restore_truncate'} value={'true'} + checked={truncate} + onChange={() => setTruncate(s => !s)} /> Remove all files and folders before restoring this backup. diff --git a/resources/scripts/components/server/startup/StartupContainer.tsx b/resources/scripts/components/server/startup/StartupContainer.tsx index 4ec8c413b..b5bb1fea5 100644 --- a/resources/scripts/components/server/startup/StartupContainer.tsx +++ b/resources/scripts/components/server/startup/StartupContainer.tsx @@ -78,7 +78,7 @@ const StartupContainer = () => { /> : -

+

@@ -86,7 +86,7 @@ const StartupContainer = () => {

- + {data.dockerImages.length > 1 && !isCustomImage ? <> diff --git a/resources/scripts/routers/DashboardRouter.tsx b/resources/scripts/routers/DashboardRouter.tsx index 933327b53..513cc3fa8 100644 --- a/resources/scripts/routers/DashboardRouter.tsx +++ b/resources/scripts/routers/DashboardRouter.tsx @@ -21,10 +21,18 @@ export default ({ location }: RouteComponentProps) => ( } - - - - + + + + + + + + + + + + diff --git a/tests/Integration/Api/Client/TwoFactorControllerTest.php b/tests/Integration/Api/Client/TwoFactorControllerTest.php index 8af7e31a8..903efb853 100644 --- a/tests/Integration/Api/Client/TwoFactorControllerTest.php +++ b/tests/Integration/Api/Client/TwoFactorControllerTest.php @@ -101,6 +101,11 @@ class TwoFactorControllerTest extends ClientApiIntegrationTestCase $tokens = RecoveryToken::query()->where('user_id', $user->id)->get(); $this->assertCount(10, $tokens); $this->assertStringStartsWith('$2y$10$', $tokens[0]->token); + // Ensure the recovery tokens that were created include a "created_at" timestamp + // value on them. + // + // @see https://github.com/pterodactyl/panel/issues/3163 + $this->assertNotNull($tokens[0]->created_at); $tokens = $tokens->pluck('token')->toArray(); diff --git a/tests/Integration/Services/Servers/BuildModificationServiceTest.php b/tests/Integration/Services/Servers/BuildModificationServiceTest.php index 359dff67e..9a61caa34 100644 --- a/tests/Integration/Services/Servers/BuildModificationServiceTest.php +++ b/tests/Integration/Services/Servers/BuildModificationServiceTest.php @@ -3,12 +3,17 @@ namespace Pterodactyl\Tests\Integration\Services\Servers; use Mockery; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Response; use Pterodactyl\Models\Server; use Pterodactyl\Models\Allocation; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Exception\TransferException; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Tests\Integration\IntegrationTestCase; use Pterodactyl\Repositories\Wings\DaemonServerRepository; use Pterodactyl\Services\Servers\BuildModificationService; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; class BuildModificationServiceTest extends IntegrationTestCase { @@ -149,6 +154,30 @@ class BuildModificationServiceTest extends IntegrationTestCase $this->assertSame(20, $response->allocation_limit); } + /** + * Test that an exception when connecting to the Wings instance is properly ignored + * when making updates. This allows for a server to be modified even when the Wings + * node is offline. + */ + public function testConnectionExceptionIsIgnoredWhenUpdatingServerSettings() + { + $server = $this->createServerModel(); + + $this->daemonServerRepository->expects('setServer->update')->andThrows( + new DaemonConnectionException( + new RequestException('Bad request', new Request('GET', '/test'), new Response()) + ) + ); + + $response = $this->getService()->handle($server, ['memory' => 256, 'disk' => 10240]); + + $this->assertInstanceOf(Server::class, $response); + $this->assertSame(256, $response->memory); + $this->assertSame(10240, $response->disk); + + $this->assertDatabaseHas('servers', ['id' => $response->id, 'memory' => 256, 'disk' => 10240]); + } + /** * Test that no exception is thrown if we are only removing an allocation. */ @@ -215,7 +244,9 @@ class BuildModificationServiceTest extends IntegrationTestCase /** * Test that any changes we made to the server or allocations are rolled back if there is an - * exception while performing any action. + * exception while performing any action. This is different than the connection exception + * test which should properly ignore connection issues. We want any other type of exception + * to properly be thrown back to the caller. */ public function testThatUpdatesAreRolledBackIfExceptionIsEncountered() {