Merge branch 'v2' into dane/webauthn

This commit is contained in:
Dane Everitt 2022-02-13 13:46:15 -05:00
commit 8971e78ab5
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
269 changed files with 6005 additions and 3308 deletions

View file

@ -5,6 +5,7 @@ APP_THEME=pterodactyl
APP_TIMEZONE=America/Los_Angeles
APP_URL=http://localhost/
DB_CONNECTION=testing
TESTING_DB_HOST=127.0.0.1
TESTING_DB_DATABASE=panel_test
TESTING_DB_USERNAME=root

View file

@ -8,6 +8,7 @@ APP_DELETE_MINUTES=10
APP_ENVIRONMENT_ONLY=true
LOG_CHANNEL=daily
APP_LOCALE=en
APP_URL=http://panel.example.com
DB_HOST=127.0.0.1
DB_PORT=3306
@ -30,7 +31,7 @@ MAILGUN_ENDPOINT=api.mailgun.net
# mail servers such as Gmail to reject your mail.
#
# @see: https://github.com/pterodactyl/panel/pull/3110
# SERVER_NAME=panel.yourdomain.com
# SERVER_NAME=panel.example.com
QUEUE_HIGH=high
QUEUE_STANDARD=standard

View file

@ -43,6 +43,10 @@ rules:
array-bracket-spacing:
- warn
- always
"@typescript-eslint/no-unused-vars":
- warn
- argsIgnorePattern: '^_'
varsIgnorePattern: '^_'
# Remove errors for not having newlines between operands of ternary expressions https://eslint.org/docs/rules/multiline-ternary
multiline-ternary: 0
"react-hooks/rules-of-hooks":

View file

@ -7,14 +7,6 @@ body:
value: |
Bug reports should only be used for reporting issues with how the software works. For assistance installing this software, as well as debugging issues with dependencies, please use our [Discord server](https://discord.gg/pterodactyl).
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please [search here](https://github.com/pterodactyl/panel/issues) to see if an issue already exists for your problem.
options:
- label: I have searched the existing issues before opening this issue.
required: true
- type: textarea
attributes:
label: Current Behavior
@ -32,7 +24,7 @@ body:
- type: textarea
attributes:
label: Steps to Reproduce
description: Please be as detailed as possible when providing steps to reproduce, failure to provide steps will likely result in this issue being closed.
description: Please be as detailed as possible when providing steps to reproduce, failure to provide steps will result in this issue being closed.
validations:
required: true
@ -53,6 +45,20 @@ body:
placeholder: 1.4.2
validations:
required: true
- type: input
id: egg-details
attributes:
label: Games and/or Eggs Affected
description: Please include the specific game(s) or egg(s) you are running into this bug with.
placeholder: Minecraft (Paper), Minecraft (Forge)
- type: input
id: docker-image
attributes:
label: Docker Image
description: The specific Docker image you are using for the game(s) above.
placeholder: ghcr.io/pterodactyl/yolks:java_17
- type: textarea
id: panel-logs
@ -67,3 +73,15 @@ body:
render: bash
validations:
required: false
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please [search here](https://github.com/pterodactyl/panel/issues) to see if an issue already exists for your problem.
options:
- label: I have searched the existing issues before opening this issue.
required: true
- label: I have provided all relevant details, including the specific game and Docker images I am using if this issue is related to running a server.
required: true
- label: I have checked in the Discord server and believe this is a bug with the software, and not a configuration issue with my specific system.
required: true

View file

@ -12,7 +12,7 @@ You can provide additional settings using a custom `.env` file or by setting the
## Setup
Start the docker container and the required dependencies (either provide existing ones or start containers as well, see the [docker-compose.yml](docker-compose.yml) file as an example).
Start the docker container and the required dependencies (either provide existing ones or start containers as well, see the [docker-compose.yml](https://github.com/pterodactyl/panel/blob/develop/docker-compose.example.yml) file as an example.
After the startup is complete you'll need to create a user.
If you are running the docker container without docker-compose, use:
@ -33,7 +33,7 @@ Note: If your `APP_URL` starts with `https://` you need to provide an `LETSENCRY
| ------------------- | ------------------------------------------------------------------------------ | -------- |
| `APP_URL` | The URL the panel will be reachable with (including protocol) | yes |
| `APP_TIMEZONE` | The timezone to use for the panel | yes |
| `LETSENCRYPT_EMAIL` | The email used for letsencrypt certificate generation | yes |
| `LE_EMAIL` | The email used for letsencrypt certificate generation | yes |
| `DB_HOST` | The host of the mysql instance | yes |
| `DB_PORT` | The port of the mysql instance | yes |
| `DB_DATABASE` | The name of the mysql database | yes |

View file

@ -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,20 +42,27 @@ 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|<domain>|$(echo $APP_URL | sed 's~http[s]*://~~g')|g" /etc/nginx/conf.d/default.conf
sed -i "s|<domain>|$(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
if [[ -z $DB_PORT ]]; then
echo -e "DB_PORT not specified, defaulting to 3306"
DB_PORT=3306
fi
## check for DB up before starting the panel
echo "Checking database status."
until nc -z -v -w30 $DB_HOST 3306
until nc -z -v -w30 $DB_HOST $DB_PORT
do
echo "Waiting for database connection..."
# wait for 1 seconds before check again

View file

@ -36,6 +36,7 @@ jobs:
if: "!contains(github.ref, 'develop')"
with:
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
- name: Release Development Build
@ -43,5 +44,6 @@ jobs:
if: "contains(github.ref, 'develop')"
with:
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

View file

@ -37,9 +37,7 @@ jobs:
path: |
~/.php_cs.cache
${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-cache-${{ matrix.php }}-${{ hashFiles('**.composer.lock') }}
restore-keys: |
${{ runner.os }}-cache-${{ matrix.php }}-
key: ${{ runner.os }}-cache-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
@ -52,16 +50,22 @@ jobs:
- name: composer install
run: composer install --prefer-dist --no-interaction --no-progress
- name: Run cs-fixer
run: vendor/bin/php-cs-fixer fix --dry-run --diff --diff-format=udiff --config .php-cs-fixer.dist.php
run: vendor/bin/php-cs-fixer fix --dry-run --diff --format=txt --config .php-cs-fixer.dist.php
continue-on-error: true
- name: Static Analysis
if: ${{ matrix.php }} == '8.0'
run: |
php artisan ide-helper:models -N
./vendor/bin/phpstan analyse --memory-limit=2G
env:
TESTING_DB_PORT: ${{ job.services.database.ports[3306] }}
- name: Execute Unit Tests
run: php artisan test tests/Unit
if: ${{ always() }}
env:
TESTING_DB_PORT: ${{ job.services.database.ports[3306] }}
TESTING_DB_USERNAME: root
- name: Execute Integration Tests
run: php artisan test tests/Integration
if: ${{ always() }}
env:
TESTING_DB_PORT: ${{ job.services.database.ports[3306] }}
TESTING_DB_USERNAME: root

View file

@ -10,6 +10,9 @@ $finder = (new Finder())
'node_modules',
'storage',
'bootstrap/cache',
'.phpstorm.meta.php',
'_ide_helper.php',
'_ide_helper_models.php',
])
->notName(['_ide_helper*']);

View file

@ -5,9 +5,9 @@ Release versions of Pterodactyl will include pre-compiled, minified, and hashed
However, if you are interested in running custom themes or making modifications to the React files you'll need a build
system in place to generate these compiled assets. To get your environment setup you'll need at minimum:
* Node.js 12
* [Yarn](https://classic.yarnpkg.com/lang/en/) v1
* [Go](https://golang.org/) 1.15.
* [Node.js](https://nodejs.org/en/) v14.x.x
* [Yarn](https://classic.yarnpkg.com/lang/en/) v1.x.x
* [Go](https://golang.org/) 1.17.x
### Install Dependencies
```bash

View file

@ -3,6 +3,62 @@ This file is a running track of new features and fixes to each version of the pa
This project follows [Semantic Versioning](http://semver.org) guidelines.
## v1.7.0
### Fixed
* Fixes typo in message shown to user when deleting a database.
* Fixes formatting of IPv6 addresses when displaying allocations to users.
* Fixes an exception thrown while trying to return error messages from API endpoints that inproperly masked the true underlying error.
* Fixes SSL certificate path generation for Let's Encrypt by ensuring they are always transformed to lowercase.
* Removes duplicate entries when creating a nested folder in the file manager.
* Fixes missing validation of Egg Author email addresses during the setup process that could cause unexpected failures later on.
* Fixes font rendering issues of the console on Firefox due to an outdated version of xterm.js being used.
* Fixes display overlap issues of the two-factor configuration form in a user's settings.
* **[security]** When authenticating using an API key a user session is now only persisted for the duration of the request before being destroyed.
### Changed
* CPU graph changed to show the maximum amount of CPU available to a server to better match how the memory graph is displayed.
### Added
* Adds support for `DB_PORT` environment variable in the Docker enterpoint for the Panel image.
* Adds suport for ARM environments in the Docker image.
* Adds a new warning modal for Steam servers shown when an invalid Game Server Login Token (GSL Token) is detected.
* Adds a new warning modal for Steam servers shown when the installation process runs out of available disk space.
* Adds a new warning modal for Minecraft servers shown when a server exceeds the maximum number of child processes.
* Adds support for displaying certain server variable fields as a checkbox when they're detected as using `boolean` or `in:0,1` validation rules.
* Adds support for Pug and Jade in the file editor.
* Adds an entry to the `robots.txt` file to correctly disallow all bot indexing.
## v1.6.6
### Fixed
* **[security]** Fixes a CSRF vulnerability for both the administrative test email endpoint and node auto-deployment token generation endpoint. [GHSA-wwgq-9jhf-qgw6](https://github.com/pterodactyl/panel/security/advisories/GHSA-wwgq-9jhf-qgw6)
### Changed
* Updates Minecraft eggs to include latest Java 17 yolk by default.
## v1.6.5
### Fixed
* Fixes broken application API endpoints due to changes introduced with session management in 1.6.4.
## v1.6.4
_This release should not be used, please use `1.6.5`. It has been pulled from our releases._
### Fixed
* Fixes a session management bug that would cause a user who signs out of one browser to be unintentionally logged out of other browser sessions when using the client API.
## v1.6.3
### Fixed
* **[Security]** Changes logout endpoint to be a POST request with CSRF-token validation to prevent a malicious actor from triggering a user logout.
* Fixes Wings receiving the wrong server suspension state when syncing servers.
### Added
* Adds additional throttling to login and password reset endpoints.
* Adds server uptime display when viewing a server console.
## v1.6.2
### Fixed
* **[Security]** Fixes an authentication bypass vulerability that could allow a malicious actor to login as another user in the Panel without knowing that user's email or password.
## v1.6.1
### Fixed
* Fixes server build modifications not being properly persisted to the database when edited.

View file

@ -2,7 +2,7 @@
# Build the assets that are needed for the frontend. This build stage is then discarded
# since we won't need NodeJS anymore in the future. This Docker image ships a final production
# level distribution of Pterodactyl.
FROM mhart/alpine-node:14
FROM --platform=$TARGETOS/$TARGETARCH mhart/alpine-node:14
WORKDIR /app
COPY . ./
RUN yarn install --frozen-lockfile \
@ -10,11 +10,11 @@ RUN yarn install --frozen-lockfile \
# Stage 1:
# Build the actual container with all of the needed PHP dependencies that will run the application.
FROM php:7.4-fpm-alpine
FROM --platform=$TARGETOS/$TARGETARCH php:7.4-fpm-alpine
WORKDIR /app
COPY . ./
COPY --from=0 /app/public/assets ./public/assets
RUN apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev certbot \
RUN apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev certbot certbot-nginx \
&& docker-php-ext-configure zip \
&& docker-php-ext-install bcmath gd pdo_mysql zip \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
@ -27,6 +27,7 @@ RUN apk add --no-cache --update ca-certificates dcron curl git supervisor tar un
RUN rm /usr/local/etc/php-fpm.conf \
&& echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root \
&& echo "0 23 * * * certbot renew --nginx --quiet" >> /var/spool/cron/crontabs/root \
&& sed -i s/ssl_session_cache/#ssl_session_cache/g /etc/nginx/nginx.conf \
&& mkdir -p /var/run/php /var/run/nginx
@ -35,5 +36,5 @@ COPY .github/docker/www.conf /usr/local/etc/php-fpm.conf
COPY .github/docker/supervisord.conf /etc/supervisord.conf
EXPOSE 80 443
ENTRYPOINT ["/bin/ash", ".github/docker/entrypoint.sh"]
ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View file

@ -1,7 +1,8 @@
# The MIT License (MIT)
```
Copyright (c) 2015 - 2021 Dane Everitt <dane@daneeveritt.com> and Contributors
Pterodactyl®
Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -6,7 +6,7 @@
![GitHub contributors](https://img.shields.io/github/contributors/pterodactyl/panel?style=for-the-badge)
# Pterodactyl Panel
Pterodactyl is an open-source game server management panel built with PHP 7, React, and Go. Designed with security
Pterodactyl® is a free, open-source game server management panel built with PHP, React, and Go. Designed with security
in mind, Pterodactyl runs all game servers in isolated Docker containers while exposing a beautiful and intuitive
UI to end users.
@ -14,6 +14,12 @@ Stop settling for less. Make game servers a first class citizen on your platform
![Image](https://cdn.pterodactyl.io/site-assets/pterodactyl_v1_demo.gif)
## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
* [Wings Documentation](https://pterodactyl.io/wings/1.0/installing.html)
* [Community Guides](https://pterodactyl.io/community/about.html)
* Or, get additional help [via Discord](https://discord.gg/pterodactyl)
## Sponsors
I would like to extend my sincere thanks to the following sponsors for helping fund Pterodactyl's developement.
[Interested in becoming a sponsor?](https://github.com/sponsors/DaneEveritt)
@ -32,21 +38,16 @@ 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.|
| [**RocketNode**](https://rocketnode.net) | RocketNode is a VPS and Game Server provider that offers the best performing VPS and Game hosting Solutions at affordable prices! |
## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
* [Wings Documentation](https://pterodactyl.io/wings/1.0/installing.html)
* [Community Guides](https://pterodactyl.io/community/about.html)
* Or, get additional help [via Discord](https://discord.gg/pterodactyl)
| [**HostEZ**](https://hostez.io) | Providing North America Valheim, Minecraft and other popular games with low latency, high uptime and maximum availability. EZ! |
### Supported Games
We support a huge variety of games by utilizing Docker containers to isolate each instance, giving you the power to
host your games across the world without having to bloat each physical machine with additional dependencies.
Pterodactyl supports a wide variety of games by utilizing Docker containers to isolate each instance. This gives
you the power to run game servers without bloating machines with a host of additional dependencies.
Some of our core supported games include:
@ -73,27 +74,6 @@ and there are plenty more games available provided by the community. Some of the
* [and many more...](https://github.com/parkervcp/eggs)
## License
```
Copyright (c) 2015 - 2021 Dane Everitt <dane@daneeveritt.com> and Contributors
Pterodactyl® Copyright © 2015 - 2022 Dane Everitt and contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
Some Javascript and CSS used within the panel are licensed under a `MIT` or `Apache 2.0` license. Please check their
respective header files for more information.
Code released under the [MIT License](./LICENSE.md).

View file

@ -5,18 +5,13 @@ 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.7.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
Please reach out directly to any project team member on Discord when reporting a security vulnerability, or you can send an email to `dane [ät] pterodactyl.io`.
Please reach out directly to any project team member on Discord when reporting a security vulnerability, or you can send an email to `dane@pterodactyl.io`.
We make every effort to respond as soon as possible, although it may take a day or two for us to sync internally and determine the severity of the report and its impact. Please, _do not_ use a public facing channel or GitHub issues to report sensitive security issues.

View file

@ -12,6 +12,7 @@ namespace Pterodactyl\Console\Commands\Environment;
use DateTimeZone;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Validation\Factory as ValidatorFactory;
use Pterodactyl\Traits\Commands\EnvironmentWriterTrait;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
@ -78,12 +79,13 @@ class AppSettingsCommand extends Command
/**
* AppSettingsCommand constructor.
*/
public function __construct(ConfigRepository $config, Kernel $command)
public function __construct(ConfigRepository $config, Kernel $command, ValidatorFactory $validator)
{
parent::__construct();
$this->command = $command;
$this->config = $config;
$this->command = $command;
$this->validator = $validator;
}
/**
@ -103,6 +105,18 @@ class AppSettingsCommand extends Command
$this->config->get('pterodactyl.service.author', 'unknown@unknown.com')
);
$validator = $this->validator->make(
['email' => $this->variables['APP_SERVICE_AUTHOR']],
['email' => 'email']
);
if ($validator->fails()) {
foreach ($validator->errors()->all() as $error) {
$this->output->error($error);
}
return 1;
}
$this->output->comment(trans('command/messages.environment.app.app_url_help'));
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
trans('command/messages.environment.app.app_url'),

View file

@ -54,15 +54,15 @@ class InfoCommand extends Command
$this->output->title('Version Information');
$this->table([], [
['Panel Version', $this->config->get('app.version')],
['Latest Version', $this->versionService->getPanel()],
['Latest Version', $this->versionService->getLatestPanel()],
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
['Unique Identifier', $this->config->get('pterodactyl.service.author')],
], 'compact');
$this->output->title('Application Configuration');
$this->table([], [
['Environment', $this->formatText($this->config->get('app.env'), $this->config->get('app.env') === 'production' ?: 'bg=red')],
['Debug Mode', $this->formatText($this->config->get('app.debug') ? 'Yes' : 'No', !$this->config->get('app.debug') ?: 'bg=red')],
['Environment', $this->formatText($this->config->get('app.env'), $this->config->get('app.env') === 'production' ? '' : 'bg=red')],
['Debug Mode', $this->formatText($this->config->get('app.debug') ? 'Yes' : 'No', !$this->config->get('app.debug') ? '' : 'bg=red')],
['Installation URL', $this->config->get('app.url')],
['Installation Directory', base_path()],
['Timezone', $this->config->get('app.timezone')],

View file

@ -1,11 +1,4 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Console\Commands\Location;
@ -26,9 +19,9 @@ class DeleteLocationCommand extends Command
protected $description = 'Deletes a location from the Panel.';
/**
* @var \Illuminate\Support\Collection
* @var \Illuminate\Support\Collection|null
*/
protected $locations;
protected $locations = null;
/**
* @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface

View file

@ -13,14 +13,14 @@ class SeedCommand extends BaseSeedCommand
* Block someone from running this seed command if they have not completed
* the migration process.
*/
public function handle()
public function handle(): int
{
if (!$this->hasCompletedMigrations()) {
$this->showMigrationWarning();
return;
return 1;
}
parent::handle();
return parent::handle();
}
}

View file

@ -13,14 +13,14 @@ class UpCommand extends BaseUpCommand
* Block someone from running this up command if they have not completed
* the migration process.
*/
public function handle()
public function handle(): int
{
if (!$this->hasCompletedMigrations()) {
$this->showMigrationWarning();
return;
return 1;
}
parent::handle();
return parent::handle();
}
}

View file

@ -24,7 +24,7 @@ class ProcessRunnableCommand extends Command
/**
* Handle command execution.
*/
public function handle()
public function handle(): int
{
$schedules = Schedule::query()->with('tasks')
->where('is_active', true)
@ -35,7 +35,7 @@ class ProcessRunnableCommand extends Command
if ($schedules->count() < 1) {
$this->line('There are no scheduled tasks for servers that need to be run.');
return;
return 0;
}
$bar = $this->output->createProgressBar(count($schedules));
@ -47,6 +47,8 @@ class ProcessRunnableCommand extends Command
}
$this->line('');
return 0;
}
/**
@ -69,7 +71,7 @@ class ProcessRunnableCommand extends Command
'schedule' => $schedule->name,
'hash' => $schedule->hashid,
]));
} catch (Throwable | Exception $exception) {
} catch (Throwable|Exception $exception) {
Log::error($exception, ['schedule_id' => $schedule->id]);
$this->error("An error was encountered while processing Schedule #{$schedule->id}: " . $exception->getMessage());

View file

@ -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 <fg=blue>[{$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 <fg=blue>[{$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,11 +86,13 @@ 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;
}
}
ini_set('output_buffering', 0);
ini_set('output_buffering', '0');
$bar = $this->output->createProgressBar($skipDownload ? 9 : 10);
$bar->start();
@ -173,8 +175,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)

View file

@ -1,11 +1,4 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Console\Commands\User;
@ -47,11 +40,9 @@ class DeleteUserCommand extends Command
}
/**
* @return bool
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function handle()
public function handle(): int
{
$search = $this->option('user') ?? $this->ask(trans('command/messages.user.search_users'));
Assert::notEmpty($search, 'Search term should be an email address, got: %s.');
@ -68,13 +59,13 @@ class DeleteUserCommand extends Command
return $this->handle();
}
return false;
return 1;
}
if ($this->input->isInteractive()) {
$tableValues = [];
foreach ($results as $user) {
$tableValues[] = [$user->id, $user->email, $user->name];
$tableValues[] = [$user->id, $user->email, $user->name_first];
}
$this->table(['User ID', 'Email', 'Name'], $tableValues);
@ -85,7 +76,7 @@ class DeleteUserCommand extends Command
if (count($results) > 1) {
$this->error(trans('command/messages.user.multiple_found'));
return false;
return 1;
}
$deleteUser = $results->first();
@ -95,5 +86,7 @@ class DeleteUserCommand extends Command
$this->deletionService->handle($deleteUser);
$this->info(trans('command/messages.user.deleted'));
}
return 0;
}
}

View file

@ -62,7 +62,7 @@ class MakeUserCommand extends Command
['UUID', $user->uuid],
['Email', $user->email],
['Username', $user->username],
['Name', $user->name],
['Name', $user->name_first],
['Admin', $user->root_admin ? 'Yes' : 'No'],
]);
}

View file

@ -39,10 +39,8 @@ interface DatabaseRepositoryInterface extends RepositoryInterface
/**
* Create a new database user on a given connection.
*
* @param $max_connections
*/
public function createUser(string $username, string $remote, string $password, string $max_connections): bool;
public function createUser(string $username, string $remote, string $password, int $max_connections): bool;
/**
* Give a specific user access to a given database.
@ -61,8 +59,6 @@ interface DatabaseRepositoryInterface extends RepositoryInterface
/**
* Drop a given user on a specific connection.
*
* @return mixed
*/
public function dropUser(string $username, string $remote): bool;
}

View file

@ -20,8 +20,6 @@ interface LocationRepositoryInterface extends RepositoryInterface
/**
* Return all of the nodes and their respective count of servers for a location.
*
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithNodes(int $id): Location;
@ -29,8 +27,6 @@ interface LocationRepositoryInterface extends RepositoryInterface
/**
* Return a location and the count of nodes in that location.
*
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithNodeCount(int $id): Location;

View file

@ -1,11 +1,4 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Contracts\Repository;
@ -16,27 +9,7 @@ interface NestRepositoryInterface extends RepositoryInterface
/**
* Return a nest or all nests with their associated eggs and variables.
*
* @param int $id
*
* @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Nest
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithEggs(int $id = null);
/**
* Return a nest or all nests and the count of eggs and servers for that nest.
*
* @return \Pterodactyl\Models\Nest|\Illuminate\Database\Eloquent\Collection
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithCounts(int $id = null);
/**
* Return a nest along with its associated eggs and the servers relation on those eggs.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithEggServers(int $id): Nest;
public function getWithEggs(int $id = null): Nest;
}

View file

@ -21,20 +21,20 @@ class Fractal extends SpatieFractal
public function createData()
{
// Set the serializer by default.
if (is_null($this->serializer)) {
if (is_null($this->serializer)) { // @phpstan-ignore-line
$this->serializer = new PterodactylSerializer();
}
// Automatically set the paginator on the response object if the
// data being provided implements a paginator.
if (is_null($this->paginator) && $this->data instanceof LengthAwarePaginator) {
if (is_null($this->paginator) && $this->data instanceof LengthAwarePaginator) { // @phpstan-ignore-line
$this->paginator = new IlluminatePaginatorAdapter($this->data);
}
// If the resource name is not set attempt to pull it off the transformer
// itself and set it automatically.
$class = is_string($this->transformer) ? new $this->transformer() : $this->transformer;
if (is_null($this->resourceName) && $class instanceof Transformer) {
if (is_null($this->resourceName) && $class instanceof Transformer) { // @phpstan-ignore-line
$this->resourceName = $class->getResourceName();
}

View file

@ -17,6 +17,6 @@ final class Time
{
$offset = round(CarbonImmutable::now($timezone)->getTimezone()->getOffset(CarbonImmutable::now('UTC')) / 3600);
return sprintf('%s%s:00', $offset > 0 ? '+' : '-', str_pad(abs($offset), 2, '0', STR_PAD_LEFT));
return sprintf('%s%s:00', $offset > 0 ? '+' : '-', str_pad((string) abs($offset), 2, '0', STR_PAD_LEFT));
}
}

View file

@ -40,7 +40,7 @@ class DatabaseController extends ApplicationApiController
*/
public function index(GetDatabasesRequest $request): array
{
$perPage = $request->query('per_page', 10);
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}

View file

@ -2,11 +2,13 @@
namespace Pterodactyl\Http\Controllers\Api\Application\Eggs;
use Ramsey\Uuid\Uuid;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Nest;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Services\Eggs\Sharing\EggExporterService;
use Pterodactyl\Transformers\Api\Application\EggTransformer;
use Pterodactyl\Http\Requests\Api\Application\Eggs\GetEggRequest;
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
@ -14,20 +16,31 @@ use Pterodactyl\Http\Requests\Api\Application\Eggs\GetEggsRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\StoreEggRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\DeleteEggRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\UpdateEggRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\ExportEggRequest;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
class EggController extends ApplicationApiController
{
private EggExporterService $eggExporterService;
public function __construct(EggExporterService $eggExporterService)
{
parent::__construct();
$this->eggExporterService = $eggExporterService;
}
/**
* Return an array of all eggs on a given nest.
*/
public function index(GetEggsRequest $request, Nest $nest): array
{
$perPage = $request->query('per_page', 10);
$perPage = (int) $request->query('per_page', '10');
if ($perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}
// @phpstan-ignore-next-line
$eggs = QueryBuilder::for(Egg::query())
->where('nest_id', '=', $nest->id)
->allowedFilters(['id', 'name', 'author'])
@ -56,11 +69,18 @@ class EggController extends ApplicationApiController
*/
public function store(StoreEggRequest $request): JsonResponse
{
$egg = Egg::query()->create($request->validated());
$validated = $request->validated();
$merged = array_merge($validated, [
'uuid' => Uuid::uuid4()->toString(),
// TODO: allow this to be set in the request, and default to config value if null or not present.
'author' => config('pterodactyl.service.author'),
]);
$egg = Egg::query()->create($merged);
return $this->fractal->item($egg)
->transformWith(EggTransformer::class)
->respond(JsonResponse::HTTP_CREATED);
->respond(Response::HTTP_CREATED);
}
/**
@ -86,4 +106,14 @@ class EggController extends ApplicationApiController
return $this->returnNoContent();
}
/**
* Exports an egg.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function export(ExportEggRequest $request, int $eggId): JsonResponse
{
return new JsonResponse($this->eggExporterService->handle($eggId));
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Application\Eggs;
use Pterodactyl\Models\Egg;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Pterodactyl\Models\EggVariable;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Eggs\Variables\VariableUpdateService;
use Pterodactyl\Services\Eggs\Variables\VariableCreationService;
use Pterodactyl\Transformers\Api\Application\EggVariableTransformer;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
use Pterodactyl\Http\Requests\Api\Application\Eggs\Variables\StoreEggVariableRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\Variables\UpdateEggVariablesRequest;
class EggVariableController extends ApplicationApiController
{
private ConnectionInterface $connection;
private VariableCreationService $variableCreationService;
private VariableUpdateService $variableUpdateService;
public function __construct(ConnectionInterface $connection, VariableCreationService $variableCreationService, VariableUpdateService $variableUpdateService)
{
parent::__construct();
$this->connection = $connection;
$this->variableCreationService = $variableCreationService;
$this->variableUpdateService = $variableUpdateService;
}
/**
* Creates a new egg variable.
*
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException
*/
public function store(StoreEggVariableRequest $request, Egg $egg): array
{
$variable = $this->variableCreationService->handle($egg->id, $request->validated());
return $this->fractal->item($variable)
->transformWith(EggVariableTransformer::class)
->toArray();
}
/**
* Updates multiple egg variables.
*
* @throws \Throwable
*/
public function update(UpdateEggVariablesRequest $request, Egg $egg): array
{
$validated = $request->validated();
$this->connection->transaction(function () use ($egg, $validated) {
foreach ($validated as $data) {
$this->variableUpdateService->handle($egg, $data);
}
});
return $this->fractal->collection($egg->refresh()->variables)
->transformWith(EggVariableTransformer::class)
->toArray();
}
/**
* Deletes a single egg variable.
*/
public function delete(Request $request, Egg $egg, EggVariable $eggVariable): Response
{
EggVariable::query()
->where('id', $eggVariable->id)
->where('egg_id', $egg->id)
->delete();
return $this->returnNoContent();
}
}

View file

@ -46,7 +46,7 @@ class LocationController extends ApplicationApiController
*/
public function index(GetLocationsRequest $request): array
{
$perPage = $request->query('per_page', 10);
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}

View file

@ -34,7 +34,7 @@ class MountController extends ApplicationApiController
*/
public function index(GetMountsRequest $request): array
{
$perPage = $request->query('per_page', 10);
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}

View file

@ -13,8 +13,8 @@ use Pterodactyl\Transformers\Api\Application\EggTransformer;
use Pterodactyl\Transformers\Api\Application\NestTransformer;
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
use Pterodactyl\Http\Requests\Api\Application\Nests\GetNestRequest;
use Pterodactyl\Http\Requests\Api\Application\Nests\GetNestsRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\ImportEggRequest;
use Pterodactyl\Http\Requests\Api\Application\Nests\GetNestsRequest;
use Pterodactyl\Http\Requests\Api\Application\Nests\StoreNestRequest;
use Pterodactyl\Http\Requests\Api\Application\Nests\DeleteNestRequest;
use Pterodactyl\Http\Requests\Api\Application\Nests\UpdateNestRequest;
@ -51,7 +51,7 @@ class NestController extends ApplicationApiController
*/
public function index(GetNestsRequest $request): array
{
$perPage = $request->query('per_page', 10);
$perPage = (int) $request->query('per_page', '10');
if ($perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}

View file

@ -42,7 +42,7 @@ class AllocationController extends ApplicationApiController
*/
public function index(GetAllocationsRequest $request, Node $node): array
{
$perPage = $request->query('per_page', 10);
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}

View file

@ -50,7 +50,7 @@ class NodeController extends ApplicationApiController
*/
public function index(GetNodesRequest $request): array
{
$perPage = $request->query('per_page', 10);
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}

View file

@ -35,7 +35,7 @@ class NodeDeploymentController extends ApplicationApiController
$nodes = $this->viableNodesService->setLocations($data['location_ids'] ?? [])
->setMemory($data['memory'])
->setDisk($data['disk'])
->handle($request->query('per_page'), $request->query('page'));
->handle($request->query('per_page'), $request->query('page')); // @phpstan-ignore-line
return $this->fractal->collection($nodes)
->transformWith(NodeTransformer::class)

View file

@ -32,7 +32,7 @@ class RoleController extends ApplicationApiController
*/
public function index(GetRolesRequest $request): array
{
$perPage = $request->query('per_page', 10);
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}

View file

@ -52,7 +52,7 @@ class ServerController extends ApplicationApiController
*/
public function index(GetServersRequest $request): array
{
$perPage = $request->query('per_page', 10);
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}
@ -79,7 +79,7 @@ class ServerController extends ApplicationApiController
*/
public function store(StoreServerRequest $request): JsonResponse
{
$server = $this->creationService->handle($request->validated(), $request->getDeploymentObject());
$server = $this->creationService->handle($request->validated());
return $this->fractal->item($server)
->transformWith(ServerTransformer::class)

View file

@ -52,7 +52,7 @@ class UserController extends ApplicationApiController
*/
public function index(GetUsersRequest $request): array
{
$perPage = $request->query('per_page', 10);
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}

View file

@ -4,7 +4,7 @@ namespace Pterodactyl\Http\Controllers\Api\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Auth\SessionGuard;
use Illuminate\Auth\AuthManager;
use Pterodactyl\Services\Users\UserUpdateService;
use Pterodactyl\Transformers\Api\Client\AccountTransformer;
use Pterodactyl\Http\Requests\Api\Client\Account\UpdateEmailRequest;
@ -12,24 +12,26 @@ use Pterodactyl\Http\Requests\Api\Client\Account\UpdatePasswordRequest;
class AccountController extends ClientApiController
{
private SessionGuard $sessionGuard;
private UserUpdateService $updateService;
/**
* @var \Illuminate\Auth\AuthManager
*/
private $sessionGuard;
/**
* AccountController constructor.
*/
public function __construct(SessionGuard $sessionGuard, UserUpdateService $updateService)
public function __construct(UserUpdateService $updateService, AuthManager $sessionGuard)
{
parent::__construct();
$this->sessionGuard = $sessionGuard;
$this->updateService = $updateService;
$this->sessionGuard = $sessionGuard;
}
/**
* Gets information about the currently authenticated user.
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function index(Request $request): array
{
@ -40,9 +42,6 @@ class AccountController extends ClientApiController
/**
* Update the authenticated user's email address.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function updateEmail(UpdateEmailRequest $request): Response
{
@ -65,9 +64,12 @@ class AccountController extends ClientApiController
// cached copy of the user that does not include the updated password. Do this
// to correctly store the new user details in the guard and allow the logout
// other devices functionality to work.
$this->sessionGuard->setUser($user);
if (method_exists($this->sessionGuard, 'setUser')) {
$this->sessionGuard->setUser($user);
}
$this->sessionGuard->logoutOtherDevices($request->input('password'));
// TODO: Find another way to do this, function doesn't exist due to API changes.
//$this->sessionGuard->logoutOtherDevices($request->input('password'));
return $this->returnNoContent();
}

View file

@ -66,7 +66,7 @@ class ClientController extends ClientApiController
$builder = $builder->whereIn('servers.id', $user->accessibleServers()->pluck('id')->all());
}
$servers = $builder->paginate(min($request->query('per_page', 50), 100))->appends($request->query());
$servers = $builder->paginate(min((int) $request->query('per_page', '50'), 100))->appends($request->query());
return $this->fractal->transformWith(new ServerTransformer())->collection($servers)->toArray();
}

View file

@ -61,7 +61,6 @@ class StartupController extends ClientApiController
*/
public function update(UpdateStartupVariableRequest $request, Server $server): array
{
/** @var \Pterodactyl\Models\EggVariable $variable */
$variable = $server->variables()->where('env_variable', $request->input('key'))->first();
if (is_null($variable) || !$variable->user_viewable) {

View file

@ -7,7 +7,7 @@ use Pterodactyl\Models\User;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\Events\Failed;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Container\Container;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\Authenticatable;
@ -17,6 +17,8 @@ abstract class AbstractLoginController extends Controller
{
use AuthenticatesUsers;
protected AuthManager $auth;
/**
* Lockout time for failed login requests.
*
@ -38,26 +40,14 @@ abstract class AbstractLoginController extends Controller
*/
protected $redirectTo = '/';
/**
* @var \Illuminate\Auth\AuthManager
*/
protected $auth;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
protected $config;
/**
* LoginController constructor.
*/
public function __construct(AuthManager $auth, Repository $config)
public function __construct()
{
$this->lockoutTime = $config->get('auth.lockout.time');
$this->maxLoginAttempts = $config->get('auth.lockout.attempts');
$this->auth = $auth;
$this->config = $config;
$this->lockoutTime = config('auth.lockout.time');
$this->maxLoginAttempts = config('auth.lockout.attempts');
$this->auth = Container::getInstance()->make(AuthManager::class);
}
/**
@ -72,7 +62,7 @@ abstract class AbstractLoginController extends Controller
$this->getField($request->input('user')) => $request->input('user'),
]);
if ($request->route()->named('auth.login-checkpoint')) {
if ($request->route()->named('auth.checkpoint') || $request->route()->named('auth.checkpoint.key')) {
throw new DisplayException($message ?? trans('auth.two_factor.checkpoint_failed'));
}
@ -84,7 +74,9 @@ abstract class AbstractLoginController extends Controller
*/
protected function sendLoginResponse(User $user, Request $request): JsonResponse
{
$request->session()->remove('auth_confirmation_token');
$request->session()->regenerate();
$this->clearLoginAttempts($request);
$this->auth->guard()->login($user, true);
@ -99,8 +91,6 @@ abstract class AbstractLoginController extends Controller
/**
* Determine if the user is logging in using an email or username,.
*
* @param string $input
*/
protected function getField(string $input = null): string
{

View file

@ -16,7 +16,6 @@ class ForgotPasswordController extends Controller
/**
* Get the response for a failed password reset link.
*
* @param \Illuminate\Http\Request
* @param string $response
*/
protected function sendResetLinkFailedResponse(Request $request, $response): JsonResponse

View file

@ -2,36 +2,35 @@
namespace Pterodactyl\Http\Controllers\Auth;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Pterodactyl\Models\User;
use Illuminate\Auth\AuthManager;
use PragmaRX\Google2FA\Google2FA;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
class LoginCheckpointController extends AbstractLoginController
{
private CacheRepository $cache;
public const TOKEN_EXPIRED_MESSAGE = 'The authentication token provided has expired, please refresh the page and try again.';
private Encrypter $encrypter;
private Google2FA $google2FA;
private ValidationFactory $validation;
/**
* LoginCheckpointController constructor.
*/
public function __construct(
AuthManager $auth,
Repository $config,
CacheRepository $cache,
Encrypter $encrypter,
Google2FA $google2FA
) {
parent::__construct($auth, $config);
public function __construct(Encrypter $encrypter, Google2FA $google2FA, ValidationFactory $validation)
{
parent::__construct();
$this->cache = $cache;
$this->encrypter = $encrypter;
$this->google2FA = $google2FA;
$this->validation = $validation;
}
/**
@ -45,6 +44,7 @@ class LoginCheckpointController extends AbstractLoginController
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
* @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function __invoke(LoginCheckpointRequest $request)
{
@ -54,18 +54,24 @@ class LoginCheckpointController extends AbstractLoginController
return;
}
$token = $request->input('confirmation_token');
$details = $request->session()->get('auth_confirmation_token');
if (!$this->hasValidSessionData($details)) {
$this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE);
return;
}
if (!hash_equals($request->input('confirmation_token') ?? '', $details['token_value'])) {
$this->sendFailedLoginResponse($request);
return;
}
try {
/** @var \Pterodactyl\Models\User $user */
$user = User::query()->findOrFail($this->cache->get($token, 0));
$user = User::query()->findOrFail($details['user_id']);
} catch (ModelNotFoundException $exception) {
$this->incrementLoginAttempts($request);
$this->sendFailedLoginResponse(
$request,
null,
'The authentication token provided has expired, please refresh the page and try again.'
);
$this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE);
return;
}
@ -79,25 +85,18 @@ class LoginCheckpointController extends AbstractLoginController
$decrypted = $this->encrypter->decrypt($user->totp_secret);
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) {
$this->cache->delete($token);
return $this->sendLoginResponse($user, $request);
}
}
$this->incrementLoginAttempts($request);
$this->sendFailedLoginResponse($request, $user, !empty($recoveryToken) ? 'The recovery token provided is not valid.' : null);
}
/**
* Determines if a given recovery token is valid for the user account. If we find a matching token
* it will be deleted from the database.
*
* @return bool
*
* @throws \Exception
*/
protected function isValidRecoveryToken(User $user, string $value)
protected function isValidRecoveryToken(User $user, string $value): bool
{
foreach ($user->recoveryTokens as $token) {
if (password_verify($value, $token->token)) {
@ -109,4 +108,37 @@ class LoginCheckpointController extends AbstractLoginController
return false;
}
protected function hasValidSessionData(array $data): bool
{
return static::isValidSessionData($this->validation, $data);
}
/**
* Determines if the data provided from the session is valid or not. This
* will return false if the data is invalid, or if more time has passed than
* was configured when the session was written.
*/
public static function isValidSessionData(ValidationFactory $validation, array $data): bool
{
$validator = $validation->make($data, [
'user_id' => 'required|integer|min:1',
'token_value' => 'required|string',
'expires_at' => 'required',
]);
if ($validator->fails()) {
return false;
}
if (!$data['expires_at'] instanceof CarbonInterface) {
return false;
}
if ($data['expires_at']->isBefore(CarbonImmutable::now())) {
return false;
}
return true;
}
}

View file

@ -5,14 +5,11 @@ namespace Pterodactyl\Http\Controllers\Auth;
use Carbon\CarbonImmutable;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Auth\AuthManager;
use Pterodactyl\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\View\View;
use Illuminate\Contracts\Config\Repository;
use LaravelWebauthn\Facades\Webauthn;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
class LoginController extends AbstractLoginController
{
@ -21,31 +18,22 @@ class LoginController extends AbstractLoginController
private const METHOD_TOTP = 'totp';
private const METHOD_WEBAUTHN = 'webauthn';
private CacheRepository $cache;
private UserRepositoryInterface $repository;
private ViewFactory $view;
/**
* LoginController constructor.
*/
public function __construct(
AuthManager $auth,
Repository $config,
CacheRepository $cache,
UserRepositoryInterface $repository,
ViewFactory $view
) {
parent::__construct($auth, $config);
public function __construct(ViewFactory $view)
{
parent::__construct();
$this->cache = $cache;
$this->repository = $repository;
$this->view = $view;
}
/**
* Handle all incoming requests for the authentication routes and render the
* base authentication view component. React will take over at this point and
* turn the login area into a SPA.
* base authentication view component. React will take over at this point and
* turn the login area into an SPA.
*/
public function index(): View
{
@ -62,9 +50,6 @@ class LoginController extends AbstractLoginController
*/
public function login(Request $request)
{
$username = $request->input('user');
$useColumn = $this->getField($username);
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
$this->sendLockoutResponse($request);
@ -72,13 +57,12 @@ class LoginController extends AbstractLoginController
return;
}
try {
/** @var \Pterodactyl\Models\User $user */
$user = $this->repository->findFirstWhere([[$useColumn, '=', $username]]);
} catch (RecordNotFoundException $exception) {
$this->sendFailedLoginResponse($request);
$username = $request->input('user');
return;
/** @var \Pterodactyl\Models\User|null $user */
$user = User::query()->where($this->getField($username), $username)->first();
if (is_null($user)) {
$this->sendFailedLoginResponse($request);
}
// Ensure that the account is using a valid username and password before trying to
@ -91,17 +75,44 @@ class LoginController extends AbstractLoginController
return;
}
if ($user->use_totp) {
$token = Str::random(64);
$this->cache->put($token, $user->id, CarbonImmutable::now()->addMinutes(5));
$useTotp = $user->use_totp;
$webauthnKeys = $user->webauthnKeys()->get();
return new JsonResponse([
'complete' => false,
'methods' => [self::METHOD_TOTP],
'confirmation_token' => $token,
]);
if (!$useTotp && count($webauthnKeys) < 1) {
return $this->sendLoginResponse($user, $request);
}
return $this->sendLoginResponse($user, $request);
$methods = [];
if ($useTotp) {
$methods[] = self::METHOD_TOTP;
}
if (count($webauthnKeys) > 0) {
$methods[] = self::METHOD_WEBAUTHN;
}
$token = Str::random(64);
$request->session()->put('auth_confirmation_token', [
'user_id' => $user->id,
'token_value' => $token,
'expires_at' => CarbonImmutable::now()->addMinutes(5),
]);
$response = [
'complete' => false,
'methods' => $methods,
'confirmation_token' => $token,
];
if (count($webauthnKeys) > 0) {
$publicKey = Webauthn::getAuthenticateData($user);
$request->session()->put(self::SESSION_PUBLICKEY_REQUEST, $publicKey);
$response['webauthn'] = [
'public_key' => $publicKey,
];
}
return new JsonResponse($response);
}
}

View file

@ -87,7 +87,7 @@ class ResetPasswordController extends Controller
* account do not automatically log them in. In those cases, send the user back to the login
* form with a note telling them their password was changed and to log back in.
*
* @param \Illuminate\Contracts\Auth\CanResetPassword|\Pterodactyl\Models\User $user
* @param \Pterodactyl\Models\User $user
* @param string $password
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException

View file

@ -54,7 +54,7 @@ class SubstituteApplicationApiBindings
try {
$this->router->substituteImplicitBindings($route = $request->route());
} catch (ModelNotFoundException $exception) {
if (isset($route) && $route->getMissing()) {
if (!empty($route) && $route->getMissing()) {
$route->getMissing()($request);
}

View file

@ -25,7 +25,7 @@ class SubstituteClientApiBindings
/**
* Perform substitution of route parameters for the Client API.
*
* @param \Illuminate\Http\Request
* @param \Illuminate\Http\Request $request
*
* @return mixed
*/
@ -76,7 +76,7 @@ class SubstituteClientApiBindings
/* @var \Illuminate\Routing\Route $route */
$this->router->substituteBindings($route = $request->route());
} catch (ModelNotFoundException $exception) {
if (isset($route) && $route->getMissing()) {
if (!empty($route) && $route->getMissing()) {
$route->getMissing()($request);
}

View file

@ -41,7 +41,7 @@ class RequireTwoFactorAuthentication
*/
public function handle(Request $request, Closure $next)
{
/** @var \Pterodactyl\Models\User $user */
/** @var \Pterodactyl\Models\User|null $user */
$user = $request->user();
$uri = rtrim($request->getRequestUri(), '/') . '/';
$current = $request->route()->getName();
@ -66,6 +66,7 @@ class RequireTwoFactorAuthentication
throw new TwoFactorAuthRequiredException();
}
// @phpstan-ignore-next-line
$this->alert->danger(trans('auth.2fa_must_be_enabled'))->flash();
return redirect()->to($this->redirectRoute);

View file

@ -0,0 +1,9 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Application\Eggs;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class ExportEggRequest extends ApplicationApiRequest
{
}

View file

@ -2,13 +2,29 @@
namespace Pterodactyl\Http\Requests\Api\Application\Eggs;
use Pterodactyl\Models\Egg;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreEggRequest extends ApplicationApiRequest
{
public function rules(array $rules = null): array
{
return $rules ?? Egg::getRules();
return [
'nest_id' => 'required|bail|numeric|exists:nests,id',
'name' => 'required|string|max:191',
'description' => 'sometimes|string|nullable',
'features' => 'sometimes|array',
'docker_images' => 'required|array|min:1',
'docker_images.*' => 'required|string',
'file_denylist' => 'sometimes|array|nullable',
'file_denylist.*' => 'sometimes|string',
'config_files' => 'required|nullable|json',
'config_startup' => 'required|nullable|json',
'config_stop' => 'required|nullable|string|max:191',
// 'config_from' => 'sometimes|nullable|numeric|exists:eggs,id',
'startup' => 'required|string',
'script_container' => 'sometimes|string',
'script_entry' => 'sometimes|string',
'script_install' => 'sometimes|string',
];
}
}

View file

@ -10,16 +10,16 @@ class UpdateEggRequest extends StoreEggRequest
'nest_id' => 'sometimes|numeric|exists:nests,id',
'name' => 'sometimes|string|max:191',
'description' => 'sometimes|string|nullable',
'features' => 'sometimes|array|nullable',
'docker_images' => 'sometimes|required|array|min:1',
'features' => 'sometimes|array',
'docker_images' => 'sometimes|array|min:1',
'docker_images.*' => 'sometimes|string',
'file_denylist' => 'sometimes|array|nullable',
'file_denylist.*' => 'sometimes|string',
'config_files' => 'sometimes|nullable|json',
'config_startup' => 'sometimes|nullable|json',
'config_stop' => 'sometimes|nullable|string|max:191',
'config_from' => 'sometimes|nullable|numeric|exists:eggs,id',
'startup' => 'sometimes|nullable|string',
// 'config_from' => 'sometimes|nullable|numeric|exists:eggs,id',
'startup' => 'sometimes|string',
'script_container' => 'sometimes|string',
'script_entry' => 'sometimes|string',
'script_install' => 'sometimes|string',

View file

@ -0,0 +1,22 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Application\Eggs\Variables;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreEggVariableRequest extends ApplicationApiRequest
{
public function rules(array $rules = null): array
{
return [
'name' => 'required|string|min:1|max:191',
'description' => 'sometimes|string|nullable',
'env_variable' => 'required|regex:/^[\w]{1,191}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
'default_value' => 'present',
'user_viewable' => 'required|boolean',
'user_editable' => 'required|boolean',
'rules' => 'bail|required|string',
];
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Application\Eggs\Variables;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class UpdateEggVariablesRequest extends ApplicationApiRequest
{
public function rules(array $rules = null): array
{
return [
'*' => 'array',
'*.id' => 'required|integer',
'*.name' => 'sometimes|string|min:1|max:191',
'*.description' => 'sometimes|string|nullable',
'*.env_variable' => 'sometimes|regex:/^[\w]{1,191}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
'*.default_value' => 'sometimes|present',
'*.user_viewable' => 'sometimes|boolean',
'*.user_editable' => 'sometimes|boolean',
'*.rules' => 'sometimes|string',
];
}
}

View file

@ -3,9 +3,6 @@
namespace Pterodactyl\Http\Requests\Api\Application\Servers;
use Pterodactyl\Models\Server;
use Illuminate\Validation\Rule;
use Illuminate\Contracts\Validation\Validator;
use Pterodactyl\Models\Objects\DeploymentObject;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreServerRequest extends ApplicationApiRequest
@ -18,15 +15,9 @@ class StoreServerRequest extends ApplicationApiRequest
'external_id' => $rules['external_id'],
'name' => $rules['name'],
'description' => array_merge(['nullable'], $rules['description']),
'user' => $rules['owner_id'],
'egg' => $rules['egg_id'],
'docker_image' => $rules['image'],
'startup' => $rules['startup'],
'environment' => 'present|array',
'skip_scripts' => 'sometimes|boolean',
'oom_disabled' => 'sometimes|boolean',
'owner_id' => $rules['owner_id'],
'node_id' => $rules['node_id'],
// Resource limitations
'limits' => 'required|array',
'limits.memory' => $rules['memory'],
'limits.swap' => $rules['swap'],
@ -34,26 +25,21 @@ class StoreServerRequest extends ApplicationApiRequest
'limits.io' => $rules['io'],
'limits.threads' => $rules['threads'],
'limits.cpu' => $rules['cpu'],
'limits.oom_killer' => 'required|boolean',
// Application Resource Limits
'feature_limits' => 'required|array',
'feature_limits.databases' => $rules['database_limit'],
'feature_limits.allocations' => $rules['allocation_limit'],
'feature_limits.backups' => $rules['backup_limit'],
'feature_limits.databases' => $rules['database_limit'],
// Placeholders for rules added in withValidator() function.
'allocation.default' => '',
'allocation.additional.*' => '',
'allocation.default' => 'required|bail|integer|exists:allocations,id',
'allocation.additional.*' => 'integer|exists:allocations,id',
// Automatic deployment rules
'deploy' => 'sometimes|required|array',
'deploy.locations' => 'array',
'deploy.locations.*' => 'integer|min:1',
'deploy.dedicated_ip' => 'required_with:deploy,boolean',
'deploy.port_range' => 'array',
'deploy.port_range.*' => 'string',
'start_on_completion' => 'sometimes|boolean',
'startup' => $rules['startup'],
'environment' => 'present|array',
'egg_id' => $rules['egg_id'],
'image' => $rules['image'],
'skip_scripts' => 'present|boolean',
];
}
@ -65,69 +51,30 @@ class StoreServerRequest extends ApplicationApiRequest
'external_id' => array_get($data, 'external_id'),
'name' => array_get($data, 'name'),
'description' => array_get($data, 'description'),
'owner_id' => array_get($data, 'user'),
'egg_id' => array_get($data, 'egg'),
'image' => array_get($data, 'docker_image'),
'startup' => array_get($data, 'startup'),
'environment' => array_get($data, 'environment'),
'owner_id' => array_get($data, 'owner_id'),
'node_id' => array_get($data, 'node_id'),
'memory' => array_get($data, 'limits.memory'),
'swap' => array_get($data, 'limits.swap'),
'disk' => array_get($data, 'limits.disk'),
'io' => array_get($data, 'limits.io'),
'cpu' => array_get($data, 'limits.cpu'),
'threads' => array_get($data, 'limits.threads'),
'skip_scripts' => array_get($data, 'skip_scripts', false),
'allocation_id' => array_get($data, 'allocation.default'),
'allocation_additional' => array_get($data, 'allocation.additional'),
'start_on_completion' => array_get($data, 'start_on_completion', false),
'database_limit' => array_get($data, 'feature_limits.databases'),
'cpu' => array_get($data, 'limits.cpu'),
'oom_disabled' => !array_get($data, 'limits.oom_killer'),
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
'backup_limit' => array_get($data, 'feature_limits.backups'),
'database_limit' => array_get($data, 'feature_limits.databases'),
'allocation_id' => array_get($data, 'allocation.default'),
'allocation_additional' => array_get($data, 'allocation.additional'),
'startup' => array_get($data, 'startup'),
'environment' => array_get($data, 'environment'),
'egg_id' => array_get($data, 'egg_id'),
'image' => array_get($data, 'image'),
'skip_scripts' => array_get($data, 'skip_scripts'),
'start_on_completion' => array_get($data, 'start_on_completion', false),
];
}
public function withValidator(Validator $validator)
{
$validator->sometimes('allocation.default', [
'required',
'integer',
'bail',
Rule::exists('allocations', 'id')->where(function ($query) {
$query->whereNull('server_id');
}),
], function ($input) {
return !($input->deploy);
});
$validator->sometimes('allocation.additional.*', [
'integer',
Rule::exists('allocations', 'id')->where(function ($query) {
$query->whereNull('server_id');
}),
], function ($input) {
return !($input->deploy);
});
$validator->sometimes('deploy.locations', 'present', function ($input) {
return $input->deploy;
});
$validator->sometimes('deploy.port_range', 'present', function ($input) {
return $input->deploy;
});
}
public function getDeploymentObject(): ?DeploymentObject
{
if (is_null($this->input('deploy'))) {
return null;
}
$object = new DeploymentObject();
$object->setDedicated($this->input('deploy.dedicated_ip', false));
$object->setLocations($this->input('deploy.locations', []));
$object->setPorts($this->input('deploy.port_range', []));
return $object;
}
}

View file

@ -55,7 +55,7 @@ class UpdateServerRequest extends ApplicationApiRequest
'io' => array_get($data, 'limits.io'),
'threads' => array_get($data, 'limits.threads'),
'cpu' => array_get($data, 'limits.cpu'),
'oom_disabled' => array_get($data, 'limits.oom_disabled'),
'oom_disabled' => !array_get($data, 'limits.oom_killer'),
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
'backup_limit' => array_get($data, 'feature_limits.backups'),

View file

@ -65,8 +65,6 @@ abstract class SubuserRequest extends ClientApiRequest
// Otherwise, get the current subuser's permission set, and ensure that the
// permissions they are trying to assign are not _more_ than the ones they
// already have.
/** @var \Pterodactyl\Models\Subuser|null $subuser */
/** @var \Pterodactyl\Services\Servers\GetUserPermissionsService $service */
$service = $this->container->make(GetUserPermissionsService::class);
if (count(array_diff($permissions, $service->handle($server, $user))) > 0) {

View file

@ -101,7 +101,7 @@ class AuditLog extends Model
* currently authenticated user if available. This model is not saved at this point, so
* you can always make modifications to it as needed before saving.
*
* @return $this
* @return self
*/
public static function instance(string $action, array $metadata, bool $isSystem = false)
{

View file

@ -2,17 +2,6 @@
namespace Pterodactyl\Models;
/**
* @property int $id
* @property string $name
* @property string $host
* @property int $port
* @property string $username
* @property string $password
* @property int|null $max_databases
* @property \Carbon\CarbonImmutable $created_at
* @property \Carbon\CarbonImmutable $updated_at
*/
class DatabaseHost extends Model
{
/**

View file

@ -2,46 +2,6 @@
namespace Pterodactyl\Models;
/**
* @property int $id
* @property string $uuid
* @property int $nest_id
* @property string $author
* @property string $name
* @property string|null $description
* @property array|null $features
* @property string $docker_image -- deprecated, use $docker_images
* @property string $update_url
* @property array $docker_images
* @property array|null $file_denylist
* @property string|null $config_files
* @property string|null $config_startup
* @property string|null $config_logs
* @property string|null $config_stop
* @property int|null $config_from
* @property string|null $startup
* @property bool $script_is_privileged
* @property string|null $script_install
* @property string $script_entry
* @property string $script_container
* @property int|null $copy_script_from
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property string|null $copy_script_install
* @property string $copy_script_entry
* @property string $copy_script_container
* @property string|null $inherit_config_files
* @property string|null $inherit_config_startup
* @property string|null $inherit_config_logs
* @property string|null $inherit_config_stop
* @property string $inherit_file_denylist
* @property array|null $inherit_features
* @property \Pterodactyl\Models\Nest $nest
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Server[] $servers
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\EggVariable[] $variables
* @property \Pterodactyl\Models\Egg|null $scriptFrom
* @property \Pterodactyl\Models\Egg|null $configFrom
*/
class Egg extends Model
{
/**
@ -75,14 +35,16 @@ class Egg extends Model
* @var array
*/
protected $fillable = [
'nest_id',
'uuid',
'name',
'description',
'features',
'author',
'docker_images',
'file_denylist',
'config_files',
'config_startup',
'config_logs',
'config_stop',
'config_from',
'startup',
@ -123,7 +85,6 @@ class Egg extends Model
'config_from' => 'sometimes|bail|nullable|numeric|exists:eggs,id',
'config_stop' => 'required_without:config_from|nullable|string|max:191',
'config_startup' => 'required_without:config_from|nullable|json',
'config_logs' => 'required_without:config_from|nullable|json',
'config_files' => 'required_without:config_from|nullable|json',
'update_url' => 'sometimes|nullable|string',
];
@ -136,7 +97,6 @@ class Egg extends Model
'file_denylist' => null,
'config_stop' => null,
'config_startup' => null,
'config_logs' => null,
'config_files' => null,
'update_url' => null,
];
@ -164,10 +124,12 @@ class Egg extends Model
*/
public function getCopyScriptEntryAttribute()
{
// @phpstan-ignore-next-line
if (!is_null($this->script_entry) || is_null($this->copy_script_from)) {
return $this->script_entry;
}
// @phpstan-ignore-next-line
return $this->scriptFrom->script_entry;
}
@ -179,10 +141,12 @@ class Egg extends Model
*/
public function getCopyScriptContainerAttribute()
{
// @phpstan-ignore-next-line
if (!is_null($this->script_container) || is_null($this->copy_script_from)) {
return $this->script_container;
}
// @phpstan-ignore-next-line
return $this->scriptFrom->script_container;
}
@ -214,20 +178,6 @@ class Egg extends Model
return $this->configFrom->config_startup;
}
/**
* Return the log reading configuration for an egg.
*
* @return string
*/
public function getInheritConfigLogsAttribute()
{
if (!is_null($this->config_logs) || is_null($this->config_from)) {
return $this->config_logs;
}
return $this->configFrom->config_logs;
}
/**
* Return the stop command configuration for an egg.
*

View file

@ -2,26 +2,6 @@
namespace Pterodactyl\Models;
/**
* @property int $id
* @property int $egg_id
* @property string $name
* @property string $description
* @property string $env_variable
* @property string $default_value
* @property bool $user_viewable
* @property bool $user_editable
* @property string $rules
* @property \Carbon\CarbonImmutable $created_at
* @property \Carbon\CarbonImmutable $updated_at
* @property bool $required
* @property \Pterodactyl\Models\Egg $egg
* @property \Pterodactyl\Models\ServerVariable $serverVariable
*
* The "server_value" variable is only present on the object if you've loaded this model
* using the server relationship.
* @property string|null $server_value
*/
class EggVariable extends Model
{
/**

View file

@ -48,6 +48,7 @@ class MultiFieldServerFilter implements Filter
}
},
// Otherwise, just try to search for that specific port in the allocations.
// @phpstan-ignore-next-line
function (Builder $builder) use ($value) {
$builder->orWhere('allocations.port', 'LIKE', substr($value, 1) . '%');
}

View file

@ -23,22 +23,15 @@ abstract class Model extends IlluminateModel
/**
* Determines if the model should undergo data validation before it is saved
* to the database.
*
* @var bool
*/
protected $skipValidation = false;
protected bool $skipValidation = false;
/**
* The validator instance used by this model.
*
* @var \Illuminate\Validation\Validator
*/
protected $validator;
protected ?Validator $validator = null;
/**
* @var \Illuminate\Contracts\Validation\Factory
*/
protected static $validatorFactory;
protected static Factory $validatorFactory;
public static array $validationRules = [];
@ -82,6 +75,7 @@ abstract class Model extends IlluminateModel
{
$rules = $this->getKey() ? static::getRulesForUpdate($this) : static::getRules();
// @phpstan-ignore-next-line
return $this->validator ?: $this->validator = static::$validatorFactory->make(
[],
$rules,

View file

@ -8,38 +8,6 @@ use Illuminate\Container\Container;
use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Encryption\Encrypter;
/**
* @property int $id
* @property string $uuid
* @property bool $public
* @property string $name
* @property string|null $description
* @property int $location_id
* @property int|null $database_host_id
* @property string $fqdn
* @property int $listen_port_http
* @property int $public_port_http
* @property int $listen_port_sftp
* @property int $public_port_sftp
* @property string $scheme
* @property bool $behind_proxy
* @property bool $maintenance_mode
* @property int $memory
* @property int $memory_overallocate
* @property int $disk
* @property int $disk_overallocate
* @property int $upload_size
* @property string $daemon_token_id
* @property string $daemon_token
* @property string $daemon_base
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property \Pterodactyl\Models\Location $location
* @property \Pterodactyl\Models\Mount[]|\Illuminate\Database\Eloquent\Collection $mounts
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
* @property \Pterodactyl\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
* @property \Pterodactyl\Models\DatabaseHost $databaseHost
*/
class Node extends Model
{
use Notifiable;
@ -275,6 +243,7 @@ class Node extends Model
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
// @phpstan-ignore-next-line
return ($this->sum_memory + $memory) <= $memoryLimit && ($this->sum_disk + $disk) <= $diskLimit;
}
}

View file

@ -213,7 +213,7 @@ class Permission extends Model
* Returns all of the permissions available on the system for a user to
* have when controlling a server.
*
* @return \Illuminate\Database\Eloquent\Collection
* @phpstan-return \Illuminate\Support\Collection<string, array{description: string, keys: array<string, string>}>
*/
public static function permissions(): Collection
{

View file

@ -8,50 +8,6 @@ use Illuminate\Database\Query\JoinClause;
use Znck\Eloquent\Traits\BelongsToThrough;
use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException;
/**
* @property int $id
* @property string|null $external_id
* @property string $uuid
* @property string $uuidShort
* @property int $node_id
* @property string $name
* @property string $description
* @property string|null $status
* @property bool $skip_scripts
* @property int $owner_id
* @property int $memory
* @property int $swap
* @property int $disk
* @property int $io
* @property int $cpu
* @property string $threads
* @property bool $oom_disabled
* @property int $allocation_id
* @property int $nest_id
* @property int $egg_id
* @property string $startup
* @property string $image
* @property int $allocation_limit
* @property int $database_limit
* @property int $backup_limit
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property \Pterodactyl\Models\User $user
* @property \Pterodactyl\Models\Subuser[]|\Illuminate\Database\Eloquent\Collection $subusers
* @property \Pterodactyl\Models\Allocation $allocation
* @property \Pterodactyl\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
* @property \Pterodactyl\Models\Node $node
* @property \Pterodactyl\Models\Nest $nest
* @property \Pterodactyl\Models\Egg $egg
* @property \Pterodactyl\Models\EggVariable[]|\Illuminate\Database\Eloquent\Collection $variables
* @property \Pterodactyl\Models\Schedule[]|\Illuminate\Database\Eloquent\Collection $schedule
* @property \Pterodactyl\Models\Database[]|\Illuminate\Database\Eloquent\Collection $databases
* @property \Pterodactyl\Models\Location $location
* @property \Pterodactyl\Models\ServerTransfer $transfer
* @property \Pterodactyl\Models\Backup[]|\Illuminate\Database\Eloquent\Collection $backups
* @property \Pterodactyl\Models\Mount[]|\Illuminate\Database\Eloquent\Collection $mounts
* @property \Pterodactyl\Models\AuditLog[] $audits
*/
class Server extends Model
{
use BelongsToThrough;
@ -84,6 +40,7 @@ class Server extends Model
protected $attributes = [
'status' => self::STATUS_INSTALLING,
'oom_disabled' => true,
'startup' => null,
];
/**
@ -124,7 +81,7 @@ class Server extends Model
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'nest_id' => 'required|exists:nests,id',
'egg_id' => 'required|exists:eggs,id',
'startup' => 'required|string',
'startup' => 'nullable|string',
'skip_scripts' => 'sometimes|boolean',
'image' => 'required|string|max:191',
'database_limit' => 'present|nullable|integer|min:0',
@ -239,6 +196,7 @@ class Server extends Model
* Gets information for the service variables associated with this server.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
* @phpstan-return \Illuminate\Database\Eloquent\Relations\HasMany<\Pterodactyl\Models\EggVariable>
*/
public function variables()
{

View file

@ -58,7 +58,7 @@ class AccountCreated extends Notification implements ShouldQueue
public function toMail($notifiable)
{
$message = (new MailMessage())
->greeting('Hello ' . $this->user->name . '!')
->greeting('Hello ' . $this->user->name_first . '!')
->line('You are receiving this email because an account has been created for you on ' . config('app.name') . '.')
->line('Username: ' . $this->user->username)
->line('Email: ' . $this->user->email);

View file

@ -27,7 +27,7 @@ class MailTested extends Notification
{
return (new MailMessage())
->subject('Pterodactyl Test Message')
->greeting('Hello ' . $this->user->name . '!')
->greeting('Hello ' . $this->user->name_first . '!')
->line('This is a test of the Pterodactyl mail system. You\'re good to go!');
}
}

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Notifications;
use Webmozart\Assert\Assert;
use Illuminate\Bus\Queueable;
use Pterodactyl\Events\Event;
use Illuminate\Container\Container;
@ -33,6 +34,8 @@ class ServerInstalled extends Notification implements ShouldQueue, ReceivesEvent
*/
public function handle(Event $event): void
{
Assert::propertyExists($event, 'server');
$event->server->loadMissing('user');
$this->server = $event->server;

View file

@ -2,10 +2,12 @@
namespace Pterodactyl\Providers;
use Illuminate\Support\Str;
use Laravel\Sanctum\Sanctum;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Subuser;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Pterodactyl\Observers\UserObserver;
@ -30,6 +32,15 @@ class AppServiceProvider extends ServiceProvider
* @see https://laravel.com/docs/8.x/sanctum#overriding-default-models
*/
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
// If the APP_URL value is set with https:// make sure we force it here. Theoretically
// this should just work with the proxy logic, but there are a lot of cases where it
// doesn't, and it triggers a lot of support requests, so lets just head it off here.
//
// @see https://github.com/pterodactyl/panel/issues/3623
if (Str::startsWith(config('app.url') ?? '', 'https://')) {
URL::forceScheme('https');
}
}
/**

View file

@ -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);

View file

@ -21,7 +21,6 @@ class SettingsServiceProvider extends ServiceProvider
protected $keys = [
'app:name',
'app:locale',
'app:analytics',
'recaptcha:enabled',
'recaptcha:secret_key',
'recaptcha:website_key',

View file

@ -20,10 +20,12 @@ class BackupRepository extends EloquentRepository
/**
* Determines if too many backups have been generated by the server.
*
* @return \Pterodactyl\Models\Backup[]|\Illuminate\Support\Collection
* @return \Illuminate\Support\Collection
* @phpstan-return \Illuminate\Support\Collection<\Pterodactyl\Models\Backup>
*/
public function getBackupsGeneratedDuringTimespan(int $server, int $seconds = 600)
{
// @phpstan-ignore-next-line
return $this->getBuilder()
->withTrashed()
->where('server_id', $server)

View file

@ -89,10 +89,8 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
/**
* Create a new database user on a given connection.
*
* @param $max_connections
*/
public function createUser(string $username, string $remote, string $password, $max_connections): bool
public function createUser(string $username, string $remote, string $password, int $max_connections): bool
{
if (!$max_connections) {
return $this->run(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password));
@ -132,8 +130,6 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
/**
* Drop a given user on a specific connection.
*
* @return mixed
*/
public function dropUser(string $username, string $remote): bool
{

View file

@ -29,6 +29,8 @@ class EggRepository extends EloquentRepository implements EggRepositoryInterface
public function getWithVariables(int $id): Egg
{
try {
/* @noinspection PhpIncompatibleReturnTypeInspection */
// @phpstan-ignore-next-line
return $this->getBuilder()->with('variables')->findOrFail($id, $this->getColumns());
} catch (ModelNotFoundException $exception) {
throw new RecordNotFoundException();
@ -55,6 +57,8 @@ class EggRepository extends EloquentRepository implements EggRepositoryInterface
Assert::true((is_digit($value) || is_string($value)), 'First argument passed to getWithCopyAttributes must be an integer or string, received %s.');
try {
/* @noinspection PhpIncompatibleReturnTypeInspection */
// @phpstan-ignore-next-line
return $this->getBuilder()->with('scriptFrom', 'configFrom')->where($column, '=', $value)->firstOrFail($this->getColumns());
} catch (ModelNotFoundException $exception) {
throw new RecordNotFoundException();
@ -69,6 +73,8 @@ class EggRepository extends EloquentRepository implements EggRepositoryInterface
public function getWithExportAttributes(int $id): Egg
{
try {
/* @noinspection PhpIncompatibleReturnTypeInspection */
// @phpstan-ignore-next-line
return $this->getBuilder()->with('scriptFrom', 'configFrom', 'variables')->findOrFail($id, $this->getColumns());
} catch (ModelNotFoundException $exception) {
throw new RecordNotFoundException();

View file

@ -4,6 +4,7 @@ namespace Pterodactyl\Repositories\Eloquent;
use Illuminate\Http\Request;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Model;
use Illuminate\Support\Collection;
use Pterodactyl\Repositories\Repository;
use Illuminate\Database\Eloquent\Builder;
@ -25,12 +26,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
* Determines if the repository function should use filters off the request object
* present when returning results. This allows repository methods to be called in API
* context's such that we can pass through ?filter[name]=Dane&sort=desc for example.
*
* @param bool $usingFilters
*
* @return $this
*/
public function usingRequestFilters($usingFilters = true)
public function usingRequestFilters(bool $usingFilters = true): self
{
$this->useRequestFilters = $usingFilters;
@ -39,26 +36,22 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
/**
* Returns the request instance.
*
* @return \Illuminate\Http\Request
*/
protected function request()
protected function request(): Request
{
return $this->app->make(Request::class);
}
/**
* Paginate the response data based on the page para.
*
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
protected function paginate(Builder $instance, int $default = 50)
protected function paginate(Builder $instance, int $default = 50): LengthAwarePaginator
{
if (!$this->useRequestFilters) {
return $instance->paginate($default);
}
return $instance->paginate($this->request()->query('per_page', $default));
return $instance->paginate((int) $this->request()->query('per_page', (string) $default));
}
/**
@ -91,15 +84,20 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
*/
public function create(array $fields, bool $validate = true, bool $force = false)
{
/** @phpstan-var \Illuminate\Database\Eloquent\Model $instance */
$instance = $this->getBuilder()->newModelInstance();
($force) ? $instance->forceFill($fields) : $instance->fill($fields);
if (!$validate) {
$saved = $instance->skipValidation()->save();
} else {
if (!$saved = $instance->save()) {
throw new DataValidationException($instance->getValidator());
if ($instance instanceof Model) {
if (!$validate) {
$saved = $instance->skipValidation()->save();
} else {
if (!$saved = $instance->save()) {
throw new DataValidationException($instance->getValidator());
}
}
} else {
$saved = $instance->save();
}
return ($this->withFresh) ? $instance->fresh() : $saved;
@ -150,6 +148,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
*/
public function findCountWhere(array $fields): int
{
// @phpstan-ignore-next-line
return $this->getBuilder()->where($fields)->count($this->getColumns());
}
@ -191,12 +190,16 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
($force) ? $instance->forceFill($fields) : $instance->fill($fields);
if (!$validate) {
$saved = $instance->skipValidation()->save();
} else {
if (!$saved = $instance->save()) {
throw new DataValidationException($instance->getValidator());
if ($instance instanceof Model) {
if (!$validate) {
$saved = $instance->skipValidation()->save();
} else {
if (!$saved = $instance->save()) {
throw new DataValidationException($instance->getValidator());
}
}
} else {
$saved = $instance->save();
}
return ($this->withFresh) ? $instance->fresh() : $saved;
@ -245,6 +248,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
return $this->create(array_merge($where, $fields), $validate, $force);
}
// @phpstan-ignore-next-line
return $this->update($instance->id, $fields, $validate, $force);
}

View file

@ -39,13 +39,13 @@ class LocationRepository extends EloquentRepository implements LocationRepositor
/**
* Return all of the nodes and their respective count of servers for a location.
*
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithNodes(int $id): Location
{
try {
/* @noinspection PhpIncompatibleReturnTypeInspection */
// @phpstan-ignore-next-line
return $this->getBuilder()->with('nodes.servers')->findOrFail($id, $this->getColumns());
} catch (ModelNotFoundException $exception) {
throw new RecordNotFoundException();
@ -55,13 +55,13 @@ class LocationRepository extends EloquentRepository implements LocationRepositor
/**
* Return a location and the count of nodes in that location.
*
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithNodeCount(int $id): Location
{
try {
/* @noinspection PhpIncompatibleReturnTypeInspection */
// @phpstan-ignore-next-line
return $this->getBuilder()->withCount('nodes')->findOrFail($id, $this->getColumns());
} catch (ModelNotFoundException $exception) {
throw new RecordNotFoundException();

View file

@ -31,13 +31,13 @@ class MountRepository extends EloquentRepository
/**
* Return all of the mounts and their respective relations.
*
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithRelations(string $id): Mount
{
try {
/* @noinspection PhpIncompatibleReturnTypeInspection */
// @phpstan-ignore-next-line
return $this->getBuilder()->with('eggs', 'nodes')->findOrFail($id, $this->getColumns());
} catch (ModelNotFoundException $exception) {
throw new RecordNotFoundException();

View file

@ -28,15 +28,14 @@ class NestRepository extends EloquentRepository implements NestRepositoryInterfa
/**
* Return a nest or all nests with their associated eggs and variables.
*
* @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Nest
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithEggs(int $id = null)
public function getWithEggs(int $id = null): Nest
{
$instance = $this->getBuilder()->with('eggs', 'eggs.variables');
if (!is_null($id)) {
/** @var \Pterodactyl\Models\Nest|null $instance */
$instance = $instance->find($id, $this->getColumns());
if (!$instance) {
throw new RecordNotFoundException();
@ -45,45 +44,8 @@ class NestRepository extends EloquentRepository implements NestRepositoryInterfa
return $instance;
}
/* @noinspection PhpIncompatibleReturnTypeInspection */
// @phpstan-ignore-next-line
return $instance->get($this->getColumns());
}
/**
* Return a nest or all nests and the count of eggs and servers for that nest.
*
* @return \Pterodactyl\Models\Nest|\Illuminate\Database\Eloquent\Collection
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithCounts(int $id = null)
{
$instance = $this->getBuilder()->withCount(['eggs', 'servers']);
if (!is_null($id)) {
$instance = $instance->find($id, $this->getColumns());
if (!$instance) {
throw new RecordNotFoundException();
}
return $instance;
}
return $instance->get($this->getColumns());
}
/**
* Return a nest along with its associated eggs and the servers relation on those eggs.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithEggServers(int $id): Nest
{
$instance = $this->getBuilder()->with('eggs.servers')->find($id, $this->getColumns());
if (!$instance) {
throw new RecordNotFoundException();
}
/* @var Nest $instance */
return $instance;
}
}

View file

@ -59,7 +59,10 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
$this->getBuilder()->raw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
)->join('servers', 'servers.node_id', '=', 'nodes.id')->where('node_id', $node->id)->first();
return collect(['disk' => $stats->sum_disk, 'memory' => $stats->sum_memory])->mapWithKeys(function ($value, $key) use ($node) {
return collect([
'disk' => $stats->sum_disk,
'memory' => $stats->sum_memory,
])->mapWithKeys(function ($value, $key) use ($node) {
$maxUsage = $node->{$key};
if ($node->{$key . '_overallocate'} > 0) {
$maxUsage = $node->{$key} * (1 + ($node->{$key . '_overallocate'} / 100));
@ -85,6 +88,7 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
// This is quite ugly and can probably be improved down the road.
// And by probably, I mean it should.
// @phpstan-ignore-next-line
if (is_null($node->servers_count) || $refresh) {
$node->load('servers');
$node->setRelation('servers_count', count($node->getRelation('servers')));
@ -118,22 +122,28 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
*/
public function getNodesForServerCreation(): Collection
{
return $this->getBuilder()->with('allocations')->get()->map(function (Node $item) {
/** @phpstan-var \Illuminate\Database\Eloquent\Collection<\Pterodactyl\Models\Node> $collection */
$collection = $this->getBuilder()->with('allocations')->get();
return $collection->map(function (Node $item) {
/** @phpstan-var \Illuminate\Support\Collection<array{id: string, ip: string, port: string|int}> $filtered */
$filtered = $item->getRelation('allocations')->where('server_id', null)->map(function ($map) {
return collect($map)->only(['id', 'ip', 'port']);
});
$item->ports = $filtered->map(function ($map) {
$ports = $filtered->map(function ($map) {
return [
'id' => $map['id'],
'text' => sprintf('%s:%s', $map['ip'], $map['port']),
];
})->values();
$item->setAttribute('ports', $ports);
return [
'id' => $item->id,
'text' => $item->name,
'allocations' => $item->ports,
'allocations' => $ports,
];
})->values();
}
@ -144,11 +154,22 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
public function getNodeWithResourceUsage(int $node_id): Node
{
$instance = $this->getBuilder()
->select(['nodes.id', 'nodes.fqdn', 'nodes.public_port_http', 'nodes.scheme', 'nodes.daemon_token', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
->select([
'nodes.id',
'nodes.fqdn',
'nodes.public_port_http',
'nodes.scheme',
'nodes.daemon_token',
'nodes.memory',
'nodes.disk',
'nodes.memory_overallocate',
'nodes.disk_overallocate',
])
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.id', $node_id);
/* @noinspection PhpIncompatibleReturnTypeInspection */
return $instance->first();
}
}

View file

@ -36,6 +36,8 @@ class ScheduleRepository extends EloquentRepository implements ScheduleRepositor
public function getScheduleWithTasks(int $schedule): Schedule
{
try {
/* @noinspection PhpIncompatibleReturnTypeInspection */
// @phpstan-ignore-next-line
return $this->getBuilder()->with('tasks')->findOrFail($schedule, $this->getColumns());
} catch (ModelNotFoundException $exception) {
throw new RecordNotFoundException();

View file

@ -74,6 +74,8 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
public function findWithVariables(int $id): Server
{
try {
/* @noinspection PhpIncompatibleReturnTypeInspection */
// @phpstan-ignore-next-line
return $this->getBuilder()->with('egg.variables', 'variables')
->where($this->getModel()->getKeyName(), '=', $id)
->firstOrFail($this->getColumns());

View file

@ -27,6 +27,8 @@ class TaskRepository extends EloquentRepository implements TaskRepositoryInterfa
public function getTaskForJobProcess(int $id): Task
{
try {
/* @noinspection PhpIncompatibleReturnTypeInspection */
// @phpstan-ignore-next-line
return $this->getBuilder()->with('server.user', 'schedule')->findOrFail($id, $this->getColumns());
} catch (ModelNotFoundException $exception) {
throw new RecordNotFoundException();

View file

@ -118,7 +118,8 @@ abstract class Repository implements RepositoryInterface
/**
* Take the provided model and make it accessible to the rest of the repository.
*
* @param array $model
* @param string[] $model
* @phpstan-param class-string<\Illuminate\Database\Eloquent\Model> $model
*
* @return mixed
*/
@ -128,6 +129,7 @@ abstract class Repository implements RepositoryInterface
case 1:
return $this->model = $this->app->make($model[0]);
case 2:
// @phpstan-ignore-next-line
return $this->model = call_user_func([$this->app->make($model[0]), $model[1]]);
default:
throw new InvalidArgumentException('Model must be a FQDN or an array with a count of two.');

View file

@ -22,7 +22,7 @@ class Username implements Rule
*/
public function passes($attribute, $value): bool
{
return preg_match(self::VALIDATION_REGEX, mb_strtolower($value));
return preg_match(self::VALIDATION_REGEX, mb_strtolower($value)) === 1;
}
/**

View file

@ -64,6 +64,7 @@ class AssignmentService
$parsed = Network::parse($underlying);
} catch (Exception $exception) {
/* @noinspection PhpUndefinedVariableInspection */
// @phpstan-ignore-next-line
throw new DisplayException("Could not parse provided allocation IP address ({$underlying}): {$exception->getMessage()}", $exception);
}

View file

@ -102,6 +102,7 @@ class DeleteBackupService
/** @var \League\Flysystem\AwsS3v3\AwsS3Adapter $adapter */
$adapter = $this->manager->adapter(Backup::ADAPTER_AWS_S3);
// @phpstan-ignore-next-line this is defined on the actual S3Client class, just not on the interface.
$adapter->getClient()->deleteObject([
'Bucket' => $adapter->getBucket(),
'Key' => sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid),

View file

@ -9,6 +9,7 @@ use Pterodactyl\Models\Backup;
use Pterodactyl\Models\Server;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Extensions\Backups\BackupManager;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Pterodactyl\Repositories\Eloquent\BackupRepository;
use Pterodactyl\Repositories\Wings\DaemonBackupRepository;
use Pterodactyl\Exceptions\Service\Backup\TooManyBackupsException;
@ -53,8 +54,6 @@ class InitiateBackupService
/**
* InitiateBackupService constructor.
*
* @param \Pterodactyl\Services\Backups\DeleteBackupService $deleteBackupService
*/
public function __construct(
BackupRepository $repository,
@ -140,7 +139,7 @@ class InitiateBackupService
// Get the oldest backup the server has that is not "locked" (indicating a backup that should
// never be automatically purged). If we find a backup we will delete it and then continue with
// this process. If no backup is found that can be used an exception is thrown.
/** @var \Pterodactyl\Models\Backup $oldest */
/** @var \Pterodactyl\Models\Backup|null $oldest */
$oldest = $successful->where('is_locked', false)->orderBy('created_at')->first();
if (!$oldest) {
throw new TooManyBackupsException($server->backup_limit);

View file

@ -152,6 +152,7 @@ class DatabaseManagementService
});
} catch (Exception $exception) {
try {
// @phpstan-ignore-next-line doesn't understand the pass-by-reference above
if ($database instanceof Database) {
$this->repository->dropDatabase($database->database);
$this->repository->dropUser($database->username, $database->remote);

View file

@ -49,8 +49,6 @@ class DatabasePasswordService
/**
* Updates a password for a given database.
*
* @param \Pterodactyl\Models\Database|int $database
*
* @throws \Throwable
*/
public function handle(Database $database): string

View file

@ -205,6 +205,7 @@ class EggConfigurationService
// Replace anything starting with "server." with the value out of the server configuration
// array that used to be created for the old daemon.
if (Str::startsWith($key, 'server.')) {
// @phpstan-ignore-next-line
$plucked = Arr::get($structure, preg_replace('/^server\./', '', $key), '');
$value = str_replace("{{{$key}}}", $plucked, $value);
@ -215,6 +216,7 @@ class EggConfigurationService
// variable from the server configuration.
$plucked = Arr::get(
$structure,
// @phpstan-ignore-next-line
preg_replace('/^env\./', 'build.env.', $key),
''
);

View file

@ -24,13 +24,11 @@ class InstallScriptService
/**
* Modify the install script for a given Egg.
*
* @param int|\Pterodactyl\Models\Egg $egg
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException
*/
public function handle(Egg $egg, array $data)
public function handle(Egg $egg, array $data): void
{
if (!is_null(array_get($data, 'copy_script_from'))) {
if (!$this->repository->isCopyableScript(array_get($data, 'copy_script_from'), $egg->nest_id)) {

View file

@ -27,11 +27,11 @@ class EggExporterService
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(int $egg): string
public function handle(int $egg): array
{
$egg = $this->repository->getWithExportAttributes($egg);
$struct = [
return [
'_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO',
'meta' => [
'version' => 'PTDL_v1',
@ -50,7 +50,6 @@ class EggExporterService
'config' => [
'files' => $egg->inherit_config_files,
'startup' => $egg->inherit_config_startup,
'logs' => $egg->inherit_config_logs,
'stop' => $egg->inherit_config_stop,
],
'scripts' => [
@ -66,7 +65,5 @@ class EggExporterService
->toArray();
}),
];
return json_encode($struct, JSON_PRETTY_PRINT);
}
}

View file

@ -14,8 +14,8 @@ use Symfony\Component\Yaml\Exception\ParseException;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException;
use Pterodactyl\Exceptions\Service\InvalidFileUploadException;
use Pterodactyl\Exceptions\Service\Egg\BadYamlFormatException;
use Pterodactyl\Exceptions\Service\InvalidFileUploadException;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
class EggImporterService
@ -58,7 +58,7 @@ class EggImporterService
/**
* Take an uploaded JSON file and parse it into a new egg.
*
* @deprecated Use `handleFile` or `handleContent` instead.
* @deprecated use `handleFile` or `handleContent` instead
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
@ -151,7 +151,6 @@ class EggImporterService
'update_url' => Arr::get($parsed, 'meta.update_url'),
'config_files' => Arr::get($parsed, 'config.files'),
'config_startup' => Arr::get($parsed, 'config.startup'),
'config_logs' => Arr::get($parsed, 'config.logs'),
'config_stop' => Arr::get($parsed, 'config.stop'),
'startup' => Arr::get($parsed, 'startup'),
'script_install' => Arr::get($parsed, 'scripts.installation.script'),

View file

@ -74,7 +74,6 @@ class EggUpdateImporterService
'docker_images' => object_get($parsed, 'images') ?? [object_get($parsed, 'image')],
'config_files' => object_get($parsed, 'config.files'),
'config_startup' => object_get($parsed, 'config.startup'),
'config_logs' => object_get($parsed, 'config.logs'),
'config_stop' => object_get($parsed, 'config.stop'),
'startup' => object_get($parsed, 'startup'),
'script_install' => object_get($parsed, 'scripts.installation.script'),

View file

@ -3,31 +3,21 @@
namespace Pterodactyl\Services\Eggs\Variables;
use Pterodactyl\Models\EggVariable;
use Illuminate\Contracts\Validation\Factory;
use Illuminate\Contracts\Validation\Factory as Validator;
use Pterodactyl\Traits\Services\ValidatesValidationRules;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException;
class VariableCreationService
{
use ValidatesValidationRules;
/**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface
*/
private $repository;
/**
* @var \Illuminate\Contracts\Validation\Factory
*/
private $validator;
private Validator $validator;
/**
* VariableCreationService constructor.
*/
public function __construct(EggVariableRepositoryInterface $repository, Factory $validator)
public function __construct(Validator $validator)
{
$this->repository = $repository;
$this->validator = $validator;
}
@ -35,7 +25,7 @@ class VariableCreationService
* Return the validation factory instance to be used by rule validation
* checking in the trait.
*/
protected function getValidator(): Factory
protected function getValidator(): Validator
{
return $this->validator;
}
@ -43,7 +33,6 @@ class VariableCreationService
/**
* Create a new variable for a given Egg.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException
*/
@ -59,15 +48,18 @@ class VariableCreationService
$options = array_get($data, 'options') ?? [];
return $this->repository->create([
/** @var \Pterodactyl\Models\EggVariable $model */
$model = EggVariable::query()->create([
'egg_id' => $egg,
'name' => $data['name'] ?? '',
'description' => $data['description'] ?? '',
'env_variable' => $data['env_variable'] ?? '',
'default_value' => $data['default_value'] ?? '',
'user_viewable' => in_array('user_viewable', $options),
'user_editable' => in_array('user_editable', $options),
'user_viewable' => $data['user_viewable'],
'user_editable' => $data['user_editable'],
'rules' => $data['rules'] ?? '',
]);
return $model;
}
}

View file

@ -3,22 +3,17 @@
namespace Pterodactyl\Services\Eggs\Variables;
use Illuminate\Support\Str;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\EggVariable;
use Illuminate\Contracts\Validation\Factory;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Traits\Services\ValidatesValidationRules;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException;
class VariableUpdateService
{
use ValidatesValidationRules;
/**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface
*/
private $repository;
/**
* @var \Illuminate\Contracts\Validation\Factory
*/
@ -27,9 +22,8 @@ class VariableUpdateService
/**
* VariableUpdateService constructor.
*/
public function __construct(EggVariableRepositoryInterface $repository, Factory $validator)
public function __construct(Factory $validator)
{
$this->repository = $repository;
$this->validator = $validator;
}
@ -45,27 +39,22 @@ class VariableUpdateService
/**
* Update a specific egg variable.
*
* @return mixed
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException
*/
public function handle(EggVariable $variable, array $data)
public function handle(Egg $egg, array $data)
{
if (!is_null(array_get($data, 'env_variable'))) {
if (in_array(strtoupper(array_get($data, 'env_variable')), explode(',', EggVariable::RESERVED_ENV_NAMES))) {
throw new ReservedVariableNameException(trans('exceptions.service.variables.reserved_name', ['name' => array_get($data, 'env_variable')]));
}
$search = $this->repository->setColumns('id')->findCountWhere([
['env_variable', '=', $data['env_variable']],
['egg_id', '=', $variable->egg_id],
['id', '!=', $variable->id],
]);
$count = $egg->variables()
->where('egg_variables.env_variable', $data['env_variable'])
->where('egg_variables.id', '!=', $data['id'])
->count();
if ($search > 0) {
if ($count > 0) {
throw new DisplayException(trans('exceptions.service.variables.env_not_unique', ['name' => array_get($data, 'env_variable')]));
}
}
@ -80,13 +69,13 @@ class VariableUpdateService
$options = array_get($data, 'options') ?? [];
return $this->repository->withoutFreshModel()->update($variable->id, [
$egg->variables()->where('egg_variables.id', $data['id'])->update([
'name' => $data['name'] ?? '',
'description' => $data['description'] ?? '',
'env_variable' => $data['env_variable'] ?? '',
'default_value' => $data['default_value'] ?? '',
'user_viewable' => in_array('user_viewable', $options),
'user_editable' => in_array('user_editable', $options),
'user_viewable' => $data['user_viewable'],
'user_editable' => $data['user_editable'],
'rules' => $data['rules'] ?? '',
]);
}

Some files were not shown because too many files have changed in this diff Show more