diff --git a/.github/docker/entrypoint.sh b/.github/docker/entrypoint.sh
index e6e1b0966..e2df6439c 100644
--- a/.github/docker/entrypoint.sh
+++ b/.github/docker/entrypoint.sh
@@ -30,7 +30,7 @@ else
fi
echo "Checking if https is required."
-if [ -f /etc/nginx/conf.d/default.conf ]; then
+if [ -f /etc/nginx/http.d/panel.conf ]; then
echo "Using nginx config already in place."
if [ $LE_EMAIL ]; then
echo "Checking for cert update"
@@ -42,15 +42,17 @@ else
echo "Checking if letsencrypt email is set."
if [ -z $LE_EMAIL ]; then
echo "No letsencrypt email is set using http config."
- cp .github/docker/default.conf /etc/nginx/conf.d/default.conf
+ cp .github/docker/default.conf /etc/nginx/http.d/panel.conf
else
echo "writing ssl config"
- cp .github/docker/default_ssl.conf /etc/nginx/conf.d/default.conf
+ cp .github/docker/default_ssl.conf /etc/nginx/http.d/panel.conf
echo "updating ssl config for domain"
- sed -i "s||$(echo $APP_URL | sed 's~http[s]*://~~g')|g" /etc/nginx/conf.d/default.conf
+ sed -i "s||$(echo $APP_URL | sed 's~http[s]*://~~g')|g" /etc/nginx/http.d/panel.conf
echo "generating certs"
certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n
fi
+ echo "Removing the default nginx config"
+ rm -rf /etc/nginx/http.d/default.conf
fi
## check for DB up before starting the panel
diff --git a/README.md b/README.md
index 42e02d5c0..6d0eec5ad 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ I would like to extend my sincere thanks to the following sponsors for helping f
| [**Spill Hosting**](https://spillhosting.no/) | Spill Hosting is a Norwegian hosting service, which aims for inexpensive services on quality servers. Premium i9-9900K processors will run your game like a dream. |
| [**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! |
+| [**Capitol Hosting Solutions**](https://chs.gg/) | 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/?utm_source=pterodactyl) | ByteAnia offers the best performing and most affordable **Ryzen 5000 Series hosting** on the market for *unbeatable prices*! |
| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. |
| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa.|
diff --git a/SECURITY.md b/SECURITY.md
index 5b412f853..a00178658 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -5,14 +5,9 @@ The following versions of Pterodactyl are receiving active support and maintenan
| Panel | Daemon | Supported |
| ----- | ------------ | ------------------ |
-| 1.4.x | wings@1.4.x | :white_check_mark: |
-| 1.3.x | wings@1.3.x | :x: |
-| 1.2.x | wings@1.2.x | :x: |
-| 1.1.x | wings@1.1.x | :x: |
-| 1.0.x | wings@1.0.x | :x: |
+| 1.6.x | wings@1.5.x | :white_check_mark: |
| 0.7.x | daemon@0.6.x | :x: |
-| 0.6.x | daemon@0.5.x | :x: |
-| 0.5.x | daemon@0.4.x | :x: |
+
## Reporting a Vulnerability
diff --git a/app/Console/Commands/UpgradeCommand.php b/app/Console/Commands/UpgradeCommand.php
index feb21ec60..224329869 100644
--- a/app/Console/Commands/UpgradeCommand.php
+++ b/app/Console/Commands/UpgradeCommand.php
@@ -57,7 +57,7 @@ class UpgradeCommand extends Command
$userDetails = posix_getpwuid(fileowner('public'));
$user = $userDetails['name'] ?? 'www-data';
- if (!$this->confirm("Your webserver user has been detected as [{$user}]: is this correct?", true)) {
+ if (!$this->confirm("Your webserver user has been detected as [{$user}]:> is this correct?", true)) {
$user = $this->anticipate(
'Please enter the name of the user running your webserver process. This varies from system to system, but is generally "www-data", "nginx", or "apache".',
[
@@ -73,7 +73,7 @@ class UpgradeCommand extends Command
$groupDetails = posix_getgrgid(filegroup('public'));
$group = $groupDetails['name'] ?? 'www-data';
- if (!$this->confirm("Your webserver group has been detected as [{$group}]: is this correct?", true)) {
+ if (!$this->confirm("Your webserver group has been detected as [{$group}]:> is this correct?", true)) {
$group = $this->anticipate(
'Please enter the name of the group running your webserver process. Normally this is the same as your user.',
[
@@ -86,6 +86,7 @@ class UpgradeCommand extends Command
}
if (!$this->confirm('Are you sure you want to run the upgrade process for your Panel?')) {
+ $this->warn('Upgrade process terminated by user.');
return;
}
}
@@ -173,8 +174,8 @@ class UpgradeCommand extends Command
$this->call('up');
});
- $this->newLine();
- $this->info('Finished running upgrade.');
+ $this->newLine(2);
+ $this->info('Panel has been successfully upgraded. Please ensure you also update any Wings instances: https://pterodactyl.io/wings/1.0/upgrading.html');
}
protected function withProgress(ProgressBar $bar, Closure $callback)
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
index fd3cf0b95..78accbc08 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -109,5 +109,7 @@ class Kernel extends HttpKernel
'bindings' => SubstituteBindings::class,
'recaptcha' => VerifyReCaptcha::class,
'node.maintenance' => MaintenanceMiddleware::class,
+ // API Specific Middleware
+ 'api..key' => AuthenticateKey::class,
];
}
diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php
index db07b1627..06100b572 100644
--- a/app/Providers/RouteServiceProvider.php
+++ b/app/Providers/RouteServiceProvider.php
@@ -2,6 +2,7 @@
namespace Pterodactyl\Providers;
+use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
@@ -19,44 +20,87 @@ class RouteServiceProvider extends ServiceProvider
protected $namespace = 'Pterodactyl\Http\Controllers';
/**
- * Define the routes for the application.
+ * Define your route model bindings, pattern filters, etc.
*/
- public function map()
+ public function boot()
{
- Route::middleware(['web', 'auth', 'csrf'])
- ->namespace($this->namespace . '\Base')
- ->group(base_path('routes/base.php'));
+ $this->configureRateLimiting();
- Route::middleware(['web', 'auth', 'admin', 'csrf'])->prefix('/admin')
- ->namespace($this->namespace . '\Admin')
- ->group(base_path('routes/admin.php'));
+ $this->routes(function () {
+ Route::middleware(['web', 'auth', 'csrf'])
+ ->namespace("$this->namespace\\Base")
+ ->group(base_path('routes/base.php'));
- Route::middleware(['web', 'csrf'])->prefix('/auth')
- ->namespace($this->namespace . '\Auth')
- ->group(base_path('routes/auth.php'));
+ Route::middleware(['web', 'auth', 'admin', 'csrf'])->prefix('/admin')
+ ->namespace("$this->namespace\\Admin")
+ ->group(base_path('routes/admin.php'));
- Route::middleware(['web', 'csrf', 'auth', 'server', 'node.maintenance'])
- ->prefix('/api/server/{server}')
- ->namespace($this->namespace . '\Server')
- ->group(base_path('routes/server.php'));
+ Route::middleware(['web', 'csrf'])->prefix('/auth')
+ ->namespace("$this->namespace\\Auth")
+ ->group(base_path('routes/auth.php'));
- Route::middleware([
- sprintf('throttle:%s,%s', config('http.rate_limit.application'), config('http.rate_limit.application_period')),
- 'api',
- ])->prefix('/api/application')
- ->namespace($this->namespace . '\Api\Application')
- ->group(base_path('routes/api-application.php'));
+ Route::middleware(['web', 'csrf', 'auth', 'server', 'node.maintenance'])
+ ->prefix('/api/server/{server}')
+ ->namespace("$this->namespace\\Server")
+ ->group(base_path('routes/server.php'));
- Route::middleware([
- //sprintf('throttle:%s,%s', config('http.rate_limit.client'), config('http.rate_limit.client_period')),
- 'client-api',
- ])->prefix('/api/client')
- ->namespace($this->namespace . '\Api\Client')
- ->group(base_path('routes/api-client.php'));
+ Route::middleware(['api', 'throttle:api.application'])
+ ->prefix('/api/application')
+ ->namespace("$this->namespace\\Api\\Application")
+ ->group(base_path('routes/api-application.php'));
- Route::middleware(['daemon'])->prefix('/api/remote')
- ->namespace($this->namespace . '\Api\Remote')
- ->group(base_path('routes/api-remote.php'));
+ Route::middleware(['client-api', 'throttle:api.client'])
+ ->prefix('/api/client')
+ ->namespace("$this->namespace\\Api\\Client")
+ ->group(base_path('routes/api-client.php'));
+
+ Route::middleware(['daemon'])->prefix('/api/remote')
+ ->namespace("$this->namespace\\Api\\Remote")
+ ->group(base_path('routes/api-remote.php'));
+ });
+ }
+
+ /**
+ * Configure the rate limiters for the application.
+ */
+ protected function configureRateLimiting()
+ {
+ // Authentication rate limiting. For login and checkpoint endpoints we'll apply
+ // a limit of 10 requests per minute, for the forgot password endpoint apply a
+ // limit of two per minute for the requester so that there is less ability to
+ // trigger email spam.
+ RateLimiter::for('authentication', function (Request $request) {
+ if ($request->route()->named('auth.post.forgot-password')) {
+ return Limit::perMinute(2)->by($request->ip());
+ }
+
+ return Limit::perMinute(10);
+ });
+
+ // Configure the throttles for both the application and client APIs below.
+ // This is configurable per-instance in "config/http.php". By default this
+ // limiter will be tied to the specific request user, and falls back to the
+ // request IP if there is no request user present for the key.
+ //
+ // This means that an authenticated API user cannot use IP switching to get
+ // around the limits.
+ RateLimiter::for('api.client', function (Request $request) {
+ $key = optional($request->user())->uuid ?: $request->ip();
+
+ return Limit::perMinutes(
+ config('http.rate_limit.client_period'),
+ config('http.rate_limit.client')
+ )->by($key);
+ });
+
+ RateLimiter::for('api.application', function (Request $request) {
+ $key = optional($request->user())->uuid ?: $request->ip();
+
+ return Limit::perMinutes(
+ config('http.rate_limit.application_period'),
+ config('http.rate_limit.application')
+ )->by($key);
+ });
RateLimiter::for('pull', function () {
return Limit::perMinute(10);
diff --git a/app/Services/Servers/SuspensionService.php b/app/Services/Servers/SuspensionService.php
index f7d0f77b1..27bc622f5 100644
--- a/app/Services/Servers/SuspensionService.php
+++ b/app/Services/Servers/SuspensionService.php
@@ -58,15 +58,20 @@ class SuspensionService
throw new ConflictHttpException('Cannot toggle suspension status on a server that is currently being transferred.');
}
- $this->connection->transaction(function () use ($action, $server, $isSuspending) {
- $server->update([
- 'status' => $isSuspending ? Server::STATUS_SUSPENDED : null,
- ]);
+ // Update the server's suspension status.
+ $server->update([
+ 'status' => $isSuspending ? Server::STATUS_SUSPENDED : null,
+ ]);
- // Only trigger a Wings server sync if it is not currently being transferred.
- if (is_null($server->transfer)) {
- $this->daemonServerRepository->setServer($server)->sync();
- }
- });
+ try {
+ // Tell wings to re-sync the server state.
+ $this->daemonServerRepository->setServer($server)->sync();
+ } catch (\Exception $exception) {
+ // Rollback the server's suspension status if wings fails to sync the server.
+ $server->update([
+ 'status' => $isSuspending ? null : Server::STATUS_SUSPENDED,
+ ]);
+ throw $exception;
+ }
}
}
diff --git a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json b/database/Seeders/eggs/minecraft/egg-forge-minecraft.json
index fa750a449..6ac6eaaf7 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-07-04T19:18:55-04:00",
+ "exported_at": "2021-10-10T07:10:13-04: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.",
@@ -18,16 +18,16 @@
"ghcr.io\/pterodactyl\/yolks:java_16"
],
"file_denylist": [],
- "startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}",
+ "startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -Dterminal.jline=false -Dterminal.ansi=true $( [ ! -f unix_args.txt ] && printf %s \"-jar {{SERVER_JARFILE}}\" || printf %s \"@unix_args.txt\" )",
"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}",
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
- "logs": "{\r\n \"custom\": false,\r\n \"location\": \"logs\/latest.log\"\r\n}",
+ "logs": "{}",
"stop": "stop"
},
"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:\/\/maven.minecraftforge.net\/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\tFILE_SITE=https:\/\/maven.minecraftforge.net\/net\/minecraftforge\/forge\/\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\nif [ ! -d \/mnt\/server ]; then\r\n mkdir \/mnt\/server\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\n# Remove spaces from the version number to avoid issues with curl\r\nFORGE_VERSION=\"$(echo \"$FORGE_VERSION\" | tr -d ' ')\"\r\nMC_VERSION=\"$(echo \"$MC_VERSION\" | tr -d ' ')\"\r\n\r\nif [ ! -z ${FORGE_VERSION} ]; then\r\n DOWNLOAD_LINK=https:\/\/maven.minecraftforge.net\/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 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=latest\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\tFILE_SITE=https:\/\/maven.minecraftforge.net\/net\/minecraftforge\/forge\/\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 of 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\n\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. Exiting now\"\r\n exit 2\r\n fi\r\nelse\r\n echo -e \"no download link provided. Exiting now\"\r\n exit 3\r\nfi\r\n\r\ncurl -s -o installer.jar -sS ${DOWNLOAD_LINK}-installer.jar\r\n\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# Delete args file to support downgrading to versions below 1.17\r\nif [ -f unix_args.txt ]; then\r\n rm unix_args.txt\r\n exit\r\nfi\r\n\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\nif [[ $MC_VERSION == *\"1.17.\"* || $MC_VERSION == *\"1.18.\"* || $FORGE_VERSION == *\"1.17.\"* || $FORGE_VERSION == *\"1.18.\"* ]]; then\r\n # Create a symlink for 1.17+ Forge args\r\n ln -sf libraries\/net\/minecraftforge\/forge\/*\/unix_args.txt unix_args.txt\r\n else\r\n # For versions below 1.17 that ship with jar delete installer.jar\r\n mv $FORGE_JAR $SERVER_JARFILE\r\n echo -e \"Deleting installer.jar file.\\n\"\r\n rm -rf installer.jar\r\nfi",
"container": "openjdk:8-jdk-slim",
"entrypoint": "bash"
}
@@ -35,7 +35,7 @@
"variables": [
{
"name": "Server Jar File",
- "description": "The name of the Jarfile to use when running Forge Mod.",
+ "description": "The name of the Jarfile to use when running Forge version below 1.17.",
"env_variable": "SERVER_JARFILE",
"default_value": "server.jar",
"user_viewable": true,
@@ -58,7 +58,7 @@
"default_value": "recommended",
"user_viewable": true,
"user_editable": true,
- "rules": "required|string|max:20"
+ "rules": "required|string|in:recommended,latest"
},
{
"name": "Forge Version",
diff --git a/database/Seeders/eggs/rust/egg-rust.json b/database/Seeders/eggs/rust/egg-rust.json
index 70eaadbad..60144bfa7 100644
--- a/database/Seeders/eggs/rust/egg-rust.json
+++ b/database/Seeders/eggs/rust/egg-rust.json
@@ -4,7 +4,7 @@
"version": "PTDL_v1",
"update_url": null
},
- "exported_at": "2021-05-29T19:02:43-04:00",
+ "exported_at": "2021-09-15T17:07:50-04:00",
"name": "Rust",
"author": "support@pterodactyl.io",
"description": "The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.",
@@ -13,11 +13,11 @@
"quay.io\/pterodactyl\/core:rust"
],
"file_denylist": [],
- "startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.logoimage \\\"{{SERVER_LOGO}}\\\" +server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} {{ADDITIONAL_ARGS}}",
+ "startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.logoimage \\\"{{SERVER_LOGO}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s \"+server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\"\" || printf %s \"+server.levelurl {{MAP_URL}}\" ) {{ADDITIONAL_ARGS}}",
"config": {
"files": "{}",
- "startup": "{\r\n \"done\": \"Server startup complete\",\r\n \"userInteraction\": []\r\n}",
- "logs": "{\r\n \"custom\": false,\r\n \"location\": \"latest.log\"\r\n}",
+ "startup": "{\r\n \"done\": \"Server startup complete\"\r\n}",
+ "logs": "{}",
"stop": "quit"
},
"scripts": {
@@ -162,6 +162,15 @@
"user_viewable": true,
"user_editable": true,
"rules": "nullable|url"
+ },
+ {
+ "name": "Custom Map URL",
+ "description": "Overwrites the map with the one from the direct download URL. Invalid URLs will cause the server to crash.",
+ "env_variable": "MAP_URL",
+ "default_value": "",
+ "user_viewable": true,
+ "user_editable": true,
+ "rules": "nullable|url"
}
]
-}
+}
\ No newline at end of file
diff --git a/resources/scripts/components/server/ServerDetailsBlock.tsx b/resources/scripts/components/server/ServerDetailsBlock.tsx
index 6df4a764c..a87dc3b01 100644
--- a/resources/scripts/components/server/ServerDetailsBlock.tsx
+++ b/resources/scripts/components/server/ServerDetailsBlock.tsx
@@ -7,14 +7,16 @@ import TitledGreyBox from '@/components/elements/TitledGreyBox';
import { ServerContext } from '@/state/server';
import CopyOnClick from '@/components/elements/CopyOnClick';
import { SocketEvent, SocketRequest } from '@/components/server/events';
+import UptimeDuration from '@/components/server/UptimeDuration';
interface Stats {
memory: number;
cpu: number;
disk: number;
+ uptime: number;
}
-function statusToColor (status: string|null, installing: boolean): TwStyle {
+function statusToColor (status: string | null, installing: boolean): TwStyle {
if (installing) {
status = '';
}
@@ -30,7 +32,7 @@ function statusToColor (status: string|null, installing: boolean): TwStyle {
}
const ServerDetailsBlock = () => {
- const [ stats, setStats ] = useState({ memory: 0, cpu: 0, disk: 0 });
+ const [ stats, setStats ] = useState({ memory: 0, cpu: 0, disk: 0, uptime: 0 });
const status = ServerContext.useStoreState(state => state.status.value);
const connected = ServerContext.useStoreState(state => state.socket.connected);
@@ -48,6 +50,7 @@ const ServerDetailsBlock = () => {
memory: stats.memory_bytes,
cpu: stats.cpu_absolute,
disk: stats.disk_bytes,
+ uptime: stats.uptime || 0,
});
};
@@ -69,7 +72,7 @@ const ServerDetailsBlock = () => {
const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring);
const limits = ServerContext.useStoreState(state => state.server.data!.limits);
const primaryAllocation = ServerContext.useStoreState(state => state.server.data!.allocations.filter(alloc => alloc.isDefault).map(
- allocation => (allocation.alias || allocation.ip) + ':' + allocation.port
+ allocation => (allocation.alias || allocation.ip) + ':' + allocation.port,
)).toString();
const diskLimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited';
@@ -88,6 +91,11 @@ const ServerDetailsBlock = () => {
]}
/>
{!status ? 'Connecting...' : (isInstalling ? 'Installing' : (isTransferring) ? 'Transferring' : status)}
+ {stats.uptime > 0 &&
+
+ ()
+
+ }
diff --git a/resources/scripts/components/server/UptimeDuration.tsx b/resources/scripts/components/server/UptimeDuration.tsx
new file mode 100644
index 000000000..1406450fa
--- /dev/null
+++ b/resources/scripts/components/server/UptimeDuration.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+export default ({ uptime }: { uptime: number }) => {
+ const hours = Math.floor(Math.floor(uptime) / 60 / 60);
+ const remainder = Math.floor(uptime - (hours * 60 * 60));
+ const minutes = Math.floor(remainder / 60);
+ const seconds = remainder % 60;
+
+ return (
+ <>
+ {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
+ >
+ );
+};
diff --git a/routes/auth.php b/routes/auth.php
index 75eb75931..ff661e3a8 100644
--- a/routes/auth.php
+++ b/routes/auth.php
@@ -1,7 +1,5 @@
'guest'], function () {
Route::get('/password', 'LoginController@index')->name('auth.forgot-password');
Route::get('/password/reset/{token}', 'LoginController@index')->name('auth.reset');
- // Login endpoints.
- Route::post('/login', 'LoginController@login')->middleware('recaptcha');
- Route::post('/login/checkpoint', 'LoginCheckpointController')->name('auth.checkpoint');
- Route::post('/login/checkpoint/key', 'WebauthnController@auth')->name('auth.checkpoint.key');
+ // Apply a throttle to authentication action endpoints, in addition to the
+ // recaptcha endpoints to slow down manual attack spammers even more. 🤷
+ //
+ // @see \Pterodactyl\Providers\RouteServiceProvider
+ Route::middleware(['throttle:authentication'])->group(function () {
+ // Login endpoints.
+ Route::post('/login', 'LoginController@login')->middleware('recaptcha');
+ Route::post('/login/checkpoint', 'LoginCheckpointController')->name('auth.login-checkpoint');
+ Route::post('/login/checkpoint/key', 'WebauthnController@auth')->name('auth.login-checkpoint-key');
- // Forgot password route. A post to this endpoint will trigger an
- // email to be sent containing a reset token.
- Route::post('/password', 'ForgotPasswordController@sendResetLinkEmail')->middleware('recaptcha');
+ // Forgot password route. A post to this endpoint will trigger an
+ // email to be sent containing a reset token.
+ Route::post('/password', 'ForgotPasswordController@sendResetLinkEmail')
+ ->name('auth.post.forgot-password')
+ ->middleware('recaptcha');
+ });
// Password reset routes. This endpoint is hit after going through
// the forgot password routes to acquire a token (or after an account