diff --git a/README.md b/README.md index 4bf5389a..d98247f9 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 a4ea4dd0..b7b819b4 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 9998a47a..a228b38c 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 618f3954..333b24a9 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 bc553392..ec3590d2 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 432f4d5f..240873ca 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 6a96075c..9a8c1c3c 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 908fc35d..28faff7f 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 e768c78d..23103f54 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 986602c1..3793d7e5 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 cba10959..3577816e 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 00000000..63448c22 --- /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 9fc4e971..1baf6b25 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 72357fd6..c04ca082 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 40eebefa..2b33dbab 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 57eefa68..c1a1db63 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 4aedfb01..77493107 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 bef06731..06f101ba 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 4ec8c413..b5bb1fea 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 933327b5..513cc3fa 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 8af7e31a..903efb85 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 359dff67..9a61caa3 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() {