forked from Alex/Pterodactyl-Panel
Compare commits
69 Commits
issue/3535
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
1d02365efe | ||
|
1564742606 | ||
|
66c56b0da8 | ||
|
15619fb8e4 | ||
|
4e6fe112b0 | ||
|
f04b87a37c | ||
|
928b060647 | ||
|
b8bf537737 | ||
|
0d5ff6afac | ||
|
5cde059f21 | ||
|
0db772a82b | ||
|
dcbc1360a9 | ||
|
622b939f00 | ||
|
e8e2911a92 | ||
|
96c3338e96 | ||
|
49d5ef271d | ||
|
4cc8658334 | ||
|
a6e0e5dbda | ||
|
10aaf00e83 | ||
|
b9d73afb63 | ||
|
59d47e746b | ||
|
01e7a45cc5 | ||
|
30bb629bad | ||
|
bf9cbe2c6d | ||
|
cc31a0a6d0 | ||
|
01871d8a6c | ||
|
17c03e9a4d | ||
|
e8a8405899 | ||
|
60eff40a0c | ||
|
d0663dcbd4 | ||
|
4dca4f0aa9 | ||
|
c4ab318d5a | ||
|
ef4410bac6 | ||
|
72680fc954 | ||
|
d65e2978d0 | ||
|
45999ba4ee | ||
|
22a8b2b3a2 | ||
|
f77932a617 | ||
|
c12f1463b0 | ||
|
5b6de4df6f | ||
|
8b236c6907 | ||
|
4fa38b8e9c | ||
|
de0d5c9b8a | ||
|
81ba333270 | ||
|
63e01f9aee | ||
|
c57eb2c9e6 | ||
|
4a84c36009 | ||
|
5fdb0a5909 | ||
|
f5a1ce13b8 | ||
|
bc25468802 | ||
|
dbb061d6f3 | ||
|
8f0eda21c5 | ||
|
52588beeb0 | ||
|
7b429831ce | ||
|
5e5d7d6689 | ||
|
4d7140bd3b | ||
|
0b521c011f | ||
|
db74b0024d | ||
|
fde0660e6c | ||
|
a775f3ccf2 | ||
|
e96ead4c4d | ||
|
869bc22103 | ||
|
2d47f986ee | ||
|
d8d1eacb42 | ||
|
7330a747b7 | ||
|
bd271e2e62 | ||
|
e1a667aaaf | ||
|
b67aceb685 | ||
|
b4cae916ac |
@ -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
|
||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: true
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Installation Help
|
||||
url: https://discord.gg/pterodactyl
|
||||
|
2
.github/docker/README.md
vendored
2
.github/docker/README.md
vendored
@ -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 |
|
||||
|
12
.github/docker/entrypoint.sh
vendored
12
.github/docker/entrypoint.sh
vendored
@ -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,22 @@ 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
|
||||
|
||||
## 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
|
||||
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@ -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 }}
|
||||
|
75
.github/workflows/tests.yml
vendored
75
.github/workflows/tests.yml
vendored
@ -1,82 +1,59 @@
|
||||
name: run tests
|
||||
name: Run Tests
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
- "release/**"
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'v2'
|
||||
pull_request:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
if: "!contains(github.event.head_commit.message, 'skip ci') && !contains(github.event.head_commit.message, 'ci skip')"
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:10.2
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: panel_test
|
||||
ports:
|
||||
- 3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
mysql:
|
||||
image: mysql:8
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: panel_test
|
||||
ports:
|
||||
- 3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [ 7.4, 8.0 ]
|
||||
database: [ mysql, mariadb ]
|
||||
name: "php-${{ matrix.php }} (engine: ${{ matrix.database }})"
|
||||
database: [ 'mariadb:10.2', 'mysql:8' ]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: panel_test
|
||||
ports:
|
||||
- 3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
name: "php-${{ matrix.php }} (${{ matrix.database }})"
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v2
|
||||
- name: get cache directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
- name: cache dependencies
|
||||
uses: actions/cache@v2
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
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 }}-
|
||||
- name: setup
|
||||
uses: shivammathur/setup-php@v2
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: cli, openssl, gd, mysql, pdo, mbstring, tokenizer, bcmath, xml, curl, zip
|
||||
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
- name: configure
|
||||
run: cp .env.ci .env
|
||||
- name: install dependencies
|
||||
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.dist
|
||||
- run: cp .env.ci .env
|
||||
- run: composer install --prefer-dist --no-interaction --no-progress
|
||||
- run: vendor/bin/php-cs-fixer fix --dry-run --diff --diff-format=udiff --config .php-cs-fixer.dist.php
|
||||
continue-on-error: true
|
||||
- name: execute unit tests
|
||||
run: vendor/bin/phpunit --bootstrap bootstrap/app.php tests/Unit
|
||||
run: vendor/bin/phpunit --bootstrap vendor/autoload.php tests/Unit
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
DB_CONNECTION: testing
|
||||
TESTING_DB_HOST: UNIT_NO_DB
|
||||
- name: execute integration tests (mysql)
|
||||
- name: execute integration tests
|
||||
run: vendor/bin/phpunit tests/Integration
|
||||
if: "${{ matrix.database }} == 'mysql'"
|
||||
env:
|
||||
TESTING_DB_PORT: ${{ job.services.mysql.ports[3306] }}
|
||||
TESTING_DB_USERNAME: root
|
||||
- name: execute integration tests (mariadb)
|
||||
run: vendor/bin/phpunit tests/Integration
|
||||
if: "${{ matrix.database }} == 'mariadb'"
|
||||
env:
|
||||
TESTING_DB_PORT: ${{ job.services.mariadb.ports[3306] }}
|
||||
TESTING_DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
TESTING_DB_USERNAME: root
|
||||
|
55
CHANGELOG.md
55
CHANGELOG.md
@ -3,6 +3,61 @@ 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.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.
|
||||
* Correctly exposes the `oom_disabled` field in the `build` limits block for a server build so that Wings can pick it up.
|
||||
*
|
||||
## v1.6.0
|
||||
### Fixed
|
||||
* Fixes array merging logic for server transfers that would cause a 500 error to occur in some scenarios.
|
||||
* Fixes user password updates not correctly logging the user out and returning a failure message even upon successful update.
|
||||
* Fixes the count of used backups when browsing a paginated backup list for a server.
|
||||
* Fixes an error being triggered when API endpoints are called with no `User-Agent` header and an audit log is generated for the action.
|
||||
* Fixes state management on the frontend not properly resetting the loading indicator when adding subusers to a server.
|
||||
* Fixes extraneous API calls being made to Wings for the server file listing when not on a file manager screen.
|
||||
|
||||
### Added
|
||||
* Adds foreign key relationship on the `mount_node`, `mount_server` and `egg_mount` tables.
|
||||
* Adds environment variable `PER_SCHEDULE_TASK_LIMIT` to allow manual overrides for the number of tasks that can exist on a single schedule. This is currently defaulted to `10`.
|
||||
* OOM killer can now be configured at the time of server creation.
|
||||
|
||||
### Changed
|
||||
* Server updates are not dependent on a successful call to Wings occurring — if the API call fails internally the error will be logged but the server update will still be persisted.
|
||||
|
||||
### Removed
|
||||
* Removed `WingsServerRepository::update()` function — if you were previously using this to modify server elements on Wings please replace calls to it with `::sync()` after updating Wings.
|
||||
|
||||
## v1.5.1
|
||||
### Fixed
|
||||
* Fixes Docker image 404ing instead of being able to access the Panel.
|
||||
|
@ -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,7 +10,7 @@ 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
|
||||
|
@ -32,11 +32,12 @@ 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! |
|
||||
| [**HostEZ**](https://hostez.io) | Providing North America Valheim, Minecraft and other popular games with low latency, high uptime and maximum availability. EZ! |
|
||||
|
||||
## Documentation
|
||||
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
|
||||
|
11
SECURITY.md
11
SECURITY.md
@ -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.6.x | wings@1.5.x | :white_check_mark: |
|
||||
| 0.7.x | daemon@0.6.x | :x: |
|
||||
| 0.6.x | daemon@0.5.x | :x: |
|
||||
| 0.5.x | daemon@0.4.x | :x: |
|
||||
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
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.
|
||||
|
||||
|
@ -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'),
|
||||
|
@ -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,6 +86,7 @@ class UpgradeCommand extends Command
|
||||
}
|
||||
|
||||
if (!$this->confirm('Are you sure you want to run the upgrade process for your Panel?')) {
|
||||
$this->warn('Upgrade process terminated by user.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -94,11 +95,6 @@ class UpgradeCommand extends Command
|
||||
$bar = $this->output->createProgressBar($skipDownload ? 9 : 10);
|
||||
$bar->start();
|
||||
|
||||
$this->withProgress($bar, function () {
|
||||
$this->line('$upgrader> php artisan down');
|
||||
$this->call('down');
|
||||
});
|
||||
|
||||
if (!$skipDownload) {
|
||||
$this->withProgress($bar, function () {
|
||||
$this->line("\$upgrader> curl -L \"{$this->getUrl()}\" | tar -xzv");
|
||||
@ -109,6 +105,11 @@ class UpgradeCommand extends Command
|
||||
});
|
||||
}
|
||||
|
||||
$this->withProgress($bar, function () {
|
||||
$this->line('$upgrader> php artisan down');
|
||||
$this->call('down');
|
||||
});
|
||||
|
||||
$this->withProgress($bar, function () {
|
||||
$this->line('$upgrader> chmod -R 755 storage bootstrap/cache');
|
||||
$process = new Process(['chmod', '-R', '755', 'storage', 'bootstrap/cache']);
|
||||
@ -173,8 +174,8 @@ class UpgradeCommand extends Command
|
||||
$this->call('up');
|
||||
});
|
||||
|
||||
$this->newLine();
|
||||
$this->info('Finished running upgrade.');
|
||||
$this->newLine(2);
|
||||
$this->info('Panel has been successfully upgraded. Please ensure you also update any Wings instances: https://pterodactyl.io/wings/1.0/upgrading.html');
|
||||
}
|
||||
|
||||
protected function withProgress(ProgressBar $bar, Closure $callback)
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace Pterodactyl\Http\Controllers\Api\Remote\Servers;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@ -87,20 +86,7 @@ class ServerTransferController extends Controller
|
||||
return $this->processFailedTransfer($server->transfer);
|
||||
}
|
||||
|
||||
// We want to generate a new configuration using the new node_id value from the
|
||||
// transfer, and not the old node value.
|
||||
$data = $this->configurationStructureService->handle($server, [
|
||||
'node_id' => $server->transfer->new_node,
|
||||
]);
|
||||
|
||||
$allocations = $server->getAllocationMappings();
|
||||
$primary = array_key_first($allocations);
|
||||
Arr::set($data, 'allocations.default.ip', $primary);
|
||||
Arr::set($data, 'allocations.default.port', $allocations[$primary][0]);
|
||||
Arr::set($data, 'service.skip_scripts', true);
|
||||
Arr::set($data, 'suspended', false);
|
||||
|
||||
$this->connection->transaction(function () use ($data, $server) {
|
||||
$this->connection->transaction(function () use ($server) {
|
||||
// This token is used by the new node the server is being transferred to. It allows
|
||||
// that node to communicate with the old node during the process to initiate the
|
||||
// actual file transfer.
|
||||
@ -119,7 +105,7 @@ class ServerTransferController extends Controller
|
||||
$this->daemonTransferRepository
|
||||
->setServer($server)
|
||||
->setNode($server->transfer->newNode)
|
||||
->notify($server, $data, $server->node, $token->toString());
|
||||
->notify($server, $token);
|
||||
});
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
@ -146,17 +132,14 @@ class ServerTransferController extends Controller
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function success(string $uuid)
|
||||
public function success(string $uuid): JsonResponse
|
||||
{
|
||||
$server = $this->repository->getByUuid($uuid);
|
||||
$transfer = $server->transfer;
|
||||
|
||||
/** @var \Pterodactyl\Models\Server $server */
|
||||
$server = $this->connection->transaction(function () use ($server, $transfer) {
|
||||
$allocations = [$transfer->old_allocation];
|
||||
if (!empty($transfer->old_additional_allocations)) {
|
||||
array_push($allocations, $transfer->old_additional_allocations);
|
||||
}
|
||||
$allocations = array_merge([$transfer->old_allocation], $transfer->old_additional_allocations);
|
||||
|
||||
// Remove the old allocations for the server and re-assign the server to the new
|
||||
// primary allocation and node.
|
||||
@ -173,7 +156,7 @@ class ServerTransferController extends Controller
|
||||
});
|
||||
|
||||
// Delete the server from the old node making sure to point it to the old node so
|
||||
// that we do not delete it from the new node the server was transfered to.
|
||||
// that we do not delete it from the new node the server was transferred to.
|
||||
try {
|
||||
$this->daemonServerRepository
|
||||
->setServer($server)
|
||||
@ -199,11 +182,7 @@ class ServerTransferController extends Controller
|
||||
$this->connection->transaction(function () use (&$transfer) {
|
||||
$transfer->forceFill(['successful' => false])->saveOrFail();
|
||||
|
||||
$allocations = [$transfer->new_allocation];
|
||||
if (!empty($transfer->new_additional_allocations)) {
|
||||
array_push($allocations, $transfer->new_additional_allocations);
|
||||
}
|
||||
|
||||
$allocations = array_merge([$transfer->new_allocation], $transfer->new_additional_allocations);
|
||||
Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]);
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,12 +74,14 @@ 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);
|
||||
|
||||
return JsonResponse::create([
|
||||
return new JsonResponse([
|
||||
'data' => [
|
||||
'complete' => true,
|
||||
'intended' => $this->redirectPath(),
|
||||
@ -101,7 +93,8 @@ abstract class AbstractLoginController extends Controller
|
||||
/**
|
||||
* Determine if the user is logging in using an email or username,.
|
||||
*
|
||||
* @param string $input
|
||||
* @param string|null $input
|
||||
* @return string
|
||||
*/
|
||||
protected function getField(string $input = null): string
|
||||
{
|
||||
|
@ -2,64 +2,36 @@
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Auth;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Pterodactyl\Models\User;
|
||||
use Illuminate\Auth\AuthManager;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
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 Pterodactyl\Contracts\Repository\UserRepositoryInterface;
|
||||
use Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository;
|
||||
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
||||
|
||||
class LoginCheckpointController extends AbstractLoginController
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Cache\Repository
|
||||
*/
|
||||
private $cache;
|
||||
private const TOKEN_EXPIRED_MESSAGE = 'The authentication token provided has expired, please refresh the page and try again.';
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
private ValidationFactory $validation;
|
||||
|
||||
/**
|
||||
* @var \PragmaRX\Google2FA\Google2FA
|
||||
*/
|
||||
private $google2FA;
|
||||
private Google2FA $google2FA;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Encryption\Encrypter
|
||||
*/
|
||||
private $encrypter;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository
|
||||
*/
|
||||
private $recoveryTokenRepository;
|
||||
private Encrypter $encrypter;
|
||||
|
||||
/**
|
||||
* LoginCheckpointController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
AuthManager $auth,
|
||||
Encrypter $encrypter,
|
||||
Google2FA $google2FA,
|
||||
Repository $config,
|
||||
CacheRepository $cache,
|
||||
RecoveryTokenRepository $recoveryTokenRepository,
|
||||
UserRepositoryInterface $repository
|
||||
) {
|
||||
parent::__construct($auth, $config);
|
||||
public function __construct(Encrypter $encrypter, Google2FA $google2FA, ValidationFactory $validation)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->google2FA = $google2FA;
|
||||
$this->cache = $cache;
|
||||
$this->repository = $repository;
|
||||
$this->encrypter = $encrypter;
|
||||
$this->recoveryTokenRepository = $recoveryTokenRepository;
|
||||
$this->validation = $validation;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,18 +53,20 @@ class LoginCheckpointController extends AbstractLoginController
|
||||
$this->sendLockoutResponse($request);
|
||||
}
|
||||
|
||||
$token = $request->input('confirmation_token');
|
||||
$details = $request->session()->get('auth_confirmation_token');
|
||||
if (!$this->hasValidSessionData($details)) {
|
||||
$this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE);
|
||||
}
|
||||
|
||||
if (!hash_equals($request->input('confirmation_token') ?? '', $details['token_value'])) {
|
||||
$this->sendFailedLoginResponse($request);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return $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);
|
||||
}
|
||||
|
||||
// Recovery tokens go through a slightly different pathway for usage.
|
||||
@ -104,15 +78,11 @@ 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);
|
||||
|
||||
return $this->sendFailedLoginResponse($request, $user, !empty($recoveryToken) ? 'The recovery token provided is not valid.' : null);
|
||||
$this->sendFailedLoginResponse($request, $user, !empty($recoveryToken) ? 'The recovery token provided is not valid.' : null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -135,4 +105,35 @@ class LoginCheckpointController extends AbstractLoginController
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasValidSessionData(array $data): bool
|
||||
{
|
||||
$validator = $this->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;
|
||||
}
|
||||
}
|
||||
|
@ -5,47 +5,24 @@ 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 Illuminate\Contracts\View\Factory as ViewFactory;
|
||||
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
||||
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
|
||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
|
||||
class LoginController extends AbstractLoginController
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\View\Factory
|
||||
*/
|
||||
private $view;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Cache\Repository
|
||||
*/
|
||||
private $cache;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
|
||||
*/
|
||||
private $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->view = $view;
|
||||
$this->cache = $cache;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,18 +45,18 @@ class LoginController extends AbstractLoginController
|
||||
*/
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$username = $request->input('user');
|
||||
$useColumn = $this->getField($username);
|
||||
|
||||
if ($this->hasTooManyLoginAttempts($request)) {
|
||||
$this->fireLockoutEvent($request);
|
||||
$this->sendLockoutResponse($request);
|
||||
}
|
||||
|
||||
try {
|
||||
$user = $this->repository->findFirstWhere([[$useColumn, '=', $username]]);
|
||||
} catch (RecordNotFoundException $exception) {
|
||||
return $this->sendFailedLoginResponse($request);
|
||||
$username = $request->input('user');
|
||||
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::query()->where($this->getField($username), $username)->firstOrFail();
|
||||
} catch (ModelNotFoundException $exception) {
|
||||
$this->sendFailedLoginResponse($request);
|
||||
}
|
||||
|
||||
// Ensure that the account is using a valid username and password before trying to
|
||||
@ -87,12 +64,17 @@ class LoginController extends AbstractLoginController
|
||||
// a flaw in which you can discover if an account exists simply by seeing if you
|
||||
// can proceede to the next step in the login process.
|
||||
if (!password_verify($request->input('password'), $user->password)) {
|
||||
return $this->sendFailedLoginResponse($request, $user);
|
||||
$this->sendFailedLoginResponse($request, $user);
|
||||
}
|
||||
|
||||
if ($user->use_totp) {
|
||||
$token = Str::random(64);
|
||||
$this->cache->put($token, $user->id, CarbonImmutable::now()->addMinutes(5));
|
||||
|
||||
$request->session()->put('auth_confirmation_token', [
|
||||
'user_id' => $user->id,
|
||||
'token_value' => $token,
|
||||
'expires_at' => CarbonImmutable::now()->addMinutes(5),
|
||||
]);
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => [
|
||||
|
@ -18,7 +18,6 @@ use Pterodactyl\Http\Middleware\LanguageMiddleware;
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
use Pterodactyl\Http\Middleware\Api\AuthenticateKey;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Pterodactyl\Http\Middleware\Api\SetSessionDriver;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
use Pterodactyl\Http\Middleware\MaintenanceMiddleware;
|
||||
@ -27,6 +26,7 @@ use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
|
||||
use Pterodactyl\Http\Middleware\Api\AuthenticateIPAccess;
|
||||
use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings;
|
||||
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
|
||||
use Pterodactyl\Http\Middleware\Api\HandleStatelessRequest;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate;
|
||||
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
|
||||
@ -68,21 +68,25 @@ class Kernel extends HttpKernel
|
||||
RequireTwoFactorAuthentication::class,
|
||||
],
|
||||
'api' => [
|
||||
HandleStatelessRequest::class,
|
||||
IsValidJson::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ApiSubstituteBindings::class,
|
||||
SetSessionDriver::class,
|
||||
'api..key:' . ApiKey::TYPE_APPLICATION,
|
||||
AuthenticateApplicationUser::class,
|
||||
VerifyCsrfToken::class,
|
||||
AuthenticateIPAccess::class,
|
||||
],
|
||||
'client-api' => [
|
||||
StartSession::class,
|
||||
SetSessionDriver::class,
|
||||
AuthenticateSession::class,
|
||||
HandleStatelessRequest::class,
|
||||
IsValidJson::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
SubstituteClientApiBindings::class,
|
||||
'api..key:' . ApiKey::TYPE_ACCOUNT,
|
||||
AuthenticateIPAccess::class,
|
||||
VerifyCsrfToken::class,
|
||||
// This is perhaps a little backwards with the Client API, but logically you'd be unable
|
||||
// to create/get an API key without first enabling 2FA on the account, so I suppose in the
|
||||
// end it makes sense.
|
||||
@ -112,7 +116,6 @@ class Kernel extends HttpKernel
|
||||
'bindings' => SubstituteBindings::class,
|
||||
'recaptcha' => VerifyReCaptcha::class,
|
||||
'node.maintenance' => MaintenanceMiddleware::class,
|
||||
|
||||
// API Specific Middleware
|
||||
'api..key' => AuthenticateKey::class,
|
||||
];
|
||||
|
@ -8,6 +8,7 @@ use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\ApiKey;
|
||||
use Illuminate\Auth\AuthManager;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||
@ -42,8 +43,10 @@ class AuthenticateKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an API request by verifying that the provided API key
|
||||
* is in a valid format and exists in the database.
|
||||
* Handle an API request by verifying that the provided API key is in a valid
|
||||
* format and exists in the database. If there is currently a user in the session
|
||||
* do not even bother to look at the token (they provided a cookie for this to
|
||||
* be the case).
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
@ -53,20 +56,20 @@ class AuthenticateKey
|
||||
public function handle(Request $request, Closure $next, int $keyType)
|
||||
{
|
||||
if (is_null($request->bearerToken()) && is_null($request->user())) {
|
||||
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
|
||||
throw new HttpException(401, 'A bearer token or valid user session cookie must be provided to access this endpoint.', null, ['WWW-Authenticate' => 'Bearer']);
|
||||
}
|
||||
|
||||
$raw = $request->bearerToken();
|
||||
|
||||
// This is a request coming through using cookies, we have an authenticated user not using
|
||||
// an API key. Make some fake API key models and continue on through the process.
|
||||
if (empty($raw) && $request->user() instanceof User) {
|
||||
// This is a request coming through using cookies, we have an authenticated user
|
||||
// not using an API key. Make some fake API key models and continue on through
|
||||
// the process.
|
||||
if ($request->user() instanceof User) {
|
||||
$model = (new ApiKey())->forceFill([
|
||||
'user_id' => $request->user()->id,
|
||||
'key_type' => ApiKey::TYPE_ACCOUNT,
|
||||
]);
|
||||
} else {
|
||||
$model = $this->authenticateApiKey($raw, $keyType);
|
||||
$model = $this->authenticateApiKey($request->bearerToken(), $keyType);
|
||||
|
||||
$this->auth->guard()->loginUsingId($model->user_id);
|
||||
}
|
||||
|
||||
|
35
app/Http/Middleware/Api/HandleStatelessRequest.php
Normal file
35
app/Http/Middleware/Api/HandleStatelessRequest.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Middleware\Api;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HandleStatelessRequest
|
||||
{
|
||||
/**
|
||||
* Ensure that the 'Set-Cookie' header is removed from the response if
|
||||
* a bearer token is present and there is an api_key in the request attributes.
|
||||
*
|
||||
* This will also delete the session from the database automatically so that
|
||||
* it is effectively treated as a stateless request. Any additional requests
|
||||
* attempting to use that session will find no data.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
/** @var \Illuminate\Http\Response $response */
|
||||
$response = $next($request);
|
||||
|
||||
if (!is_null($request->bearerToken()) && $request->isJson()) {
|
||||
$request->session()->getHandler()->destroy(
|
||||
$request->session()->getId()
|
||||
);
|
||||
|
||||
$response->headers->remove('Set-Cookie');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Middleware\Api;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||
|
||||
class SetSessionDriver
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Config\Repository
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* SetSessionDriver constructor.
|
||||
*/
|
||||
public function __construct(ConfigRepository $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the session for API calls to only last for the one request.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$this->config->set('session.driver', 'array');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
@ -2,18 +2,45 @@
|
||||
|
||||
namespace Pterodactyl\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Pterodactyl\Models\ApiKey;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
|
||||
|
||||
class VerifyCsrfToken extends BaseVerifier
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
* The URIs that should be excluded from CSRF verification. These are
|
||||
* never hit by the front-end, and require specific token validation
|
||||
* to work.
|
||||
*
|
||||
* @var array
|
||||
* @var string[]
|
||||
*/
|
||||
protected $except = [
|
||||
'remote/*',
|
||||
'daemon/*',
|
||||
'api/*',
|
||||
];
|
||||
protected $except = ['remote/*', 'daemon/*'];
|
||||
|
||||
/**
|
||||
* Manually apply CSRF protection to routes depending on the authentication
|
||||
* mechanism being used. If the API request is using an API key that exists
|
||||
* in the database we can safely ignore CSRF protections, since that would be
|
||||
* a manually initiated request by a user or server.
|
||||
*
|
||||
* All other requests should go through the standard CSRF protections that
|
||||
* Laravel affords us. This code will be removed in v2 since we have switched
|
||||
* to using Sanctum for the API endpoints, which handles that for us automatically.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Illuminate\Session\TokenMismatchException
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$key = $request->attributes->get('api_key');
|
||||
|
||||
if ($key instanceof ApiKey && $key->exists) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return parent::handle($request, $next);
|
||||
}
|
||||
}
|
||||
|
@ -120,8 +120,8 @@ class AuditLog extends Model
|
||||
'server_id' => null,
|
||||
'action' => $action,
|
||||
'device' => $request ? [
|
||||
'ip_address' => $request->getClientIp(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
'ip_address' => $request->getClientIp() ?? '127.0.0.1',
|
||||
'user_agent' => $request->userAgent() ?? '',
|
||||
] : [],
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
|
@ -2,7 +2,10 @@
|
||||
|
||||
namespace Pterodactyl\Providers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
@ -17,43 +20,86 @@ 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
@ -34,34 +35,34 @@ class DaemonServerRepository extends DaemonRepository
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function create(array $data): void
|
||||
public function create(bool $startOnCompletion = true): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->post(
|
||||
'/api/servers',
|
||||
[
|
||||
'json' => $data,
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
$this->getHttpClient()->post('/api/servers', [
|
||||
'json' => [
|
||||
'uuid' => $this->server->uuid,
|
||||
'start_on_completion' => $startOnCompletion,
|
||||
],
|
||||
]);
|
||||
} catch (GuzzleException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates details about a server on the Daemon.
|
||||
* Triggers a server sync on Wings.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function update(array $data): void
|
||||
public function sync(): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->patch('/api/servers/' . $this->server->uuid, ['json' => $data]);
|
||||
} catch (TransferException $exception) {
|
||||
$this->getHttpClient()->post("/api/servers/{$this->server->uuid}/sync");
|
||||
} catch (GuzzleException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
@ -101,26 +102,6 @@ class DaemonServerRepository extends DaemonRepository
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* By default this function will suspend a server instance on the daemon. However, passing
|
||||
* "true" as the first argument will unsuspend the server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function suspend(bool $unsuspend = false): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->patch(
|
||||
'/api/servers/' . $this->server->uuid,
|
||||
['json' => ['suspended' => !$unsuspend]]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the daemon to create a full archive of the server. Once the daemon is finished
|
||||
* they will send a POST request to "/api/remote/servers/{uuid}/archive" with a boolean.
|
||||
|
@ -2,28 +2,31 @@
|
||||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use Pterodactyl\Models\Node;
|
||||
use Lcobucci\JWT\Token\Plain;
|
||||
use Pterodactyl\Models\Server;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
class DaemonTransferRepository extends DaemonRepository
|
||||
{
|
||||
/**
|
||||
* @throws DaemonConnectionException
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function notify(Server $server, array $data, Node $node, string $token): void
|
||||
public function notify(Server $server, Plain $token): void
|
||||
{
|
||||
try {
|
||||
$this->getHttpClient()->post('/api/transfer', [
|
||||
'json' => [
|
||||
'server_id' => $server->uuid,
|
||||
'url' => $node->getConnectionAddress() . sprintf('/api/servers/%s/archive', $server->uuid),
|
||||
'token' => 'Bearer ' . $token,
|
||||
'server' => $data,
|
||||
'url' => $server->node->getConnectionAddress() . sprintf('/api/servers/%s/archive', $server->uuid),
|
||||
'token' => 'Bearer ' . $token->toString(),
|
||||
'server' => [
|
||||
'uuid' => $server->uuid,
|
||||
'start_on_completion' => false,
|
||||
],
|
||||
],
|
||||
]);
|
||||
} catch (TransferException $exception) {
|
||||
} catch (GuzzleException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ class BuildModificationService
|
||||
* BuildModificationService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $structureService
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
|
||||
*/
|
||||
public function __construct(
|
||||
ServerConfigurationStructureService $structureService,
|
||||
@ -54,48 +56,45 @@ class BuildModificationService
|
||||
*/
|
||||
public function handle(Server $server, array $data)
|
||||
{
|
||||
$this->connection->beginTransaction();
|
||||
/** @var \Pterodactyl\Models\Server $server */
|
||||
$server = $this->connection->transaction(function() use ($server, $data) {
|
||||
$this->processAllocations($server, $data);
|
||||
|
||||
$this->processAllocations($server, $data);
|
||||
|
||||
if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) {
|
||||
try {
|
||||
Allocation::query()->where('id', $data['allocation_id'])->where('server_id', $server->id)->firstOrFail();
|
||||
} catch (ModelNotFoundException $ex) {
|
||||
throw new DisplayException('The requested default allocation is not currently assigned to this server.');
|
||||
if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) {
|
||||
try {
|
||||
Allocation::query()->where('id', $data['allocation_id'])->where('server_id', $server->id)->firstOrFail();
|
||||
} catch (ModelNotFoundException $ex) {
|
||||
throw new DisplayException('The requested default allocation is not currently assigned to this server.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If any of these values are passed through in the data array go ahead and set
|
||||
// them correctly on the server model.
|
||||
$merge = Arr::only($data, ['oom_disabled', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
|
||||
// If any of these values are passed through in the data array go ahead and set
|
||||
// them correctly on the server model.
|
||||
$merge = Arr::only($data, ['oom_disabled', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
|
||||
|
||||
$server->forceFill(array_merge($merge, [
|
||||
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
|
||||
'allocation_limit' => Arr::get($data, 'allocation_limit', 0) ?? null,
|
||||
'backup_limit' => Arr::get($data, 'backup_limit', 0) ?? 0,
|
||||
]))->saveOrFail();
|
||||
$server->forceFill(array_merge($merge, [
|
||||
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
|
||||
'allocation_limit' => Arr::get($data, 'allocation_limit', 0) ?? null,
|
||||
'backup_limit' => Arr::get($data, 'backup_limit', 0) ?? 0,
|
||||
]))->saveOrFail();
|
||||
|
||||
$server = $server->fresh();
|
||||
return $server->refresh();
|
||||
});
|
||||
|
||||
$updateData = $this->structureService->handle($server);
|
||||
|
||||
// Because Wings always fetches an updated configuration from the Panel when booting
|
||||
// a server this type of exception can be safely "ignored" and just written to the logs.
|
||||
// Ideally this request succeedes so we can apply resource modifications on the fly
|
||||
// but if it fails it isn't the end of the world.
|
||||
// Ideally this request succeeds, so we can apply resource modifications on the fly, but
|
||||
// if it fails we can just continue on as normal.
|
||||
if (!empty($updateData['build'])) {
|
||||
try {
|
||||
$this->daemonServerRepository->setServer($server)->update([
|
||||
'build' => $updateData['build'],
|
||||
]);
|
||||
$this->daemonServerRepository->setServer($server)->sync();
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
Log::warning($exception, ['server_id' => $server->id]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->connection->commit();
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
|
@ -58,9 +58,13 @@ class ServerConfigurationStructureService
|
||||
'cpu_limit' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'disk_space' => $server->disk,
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
],
|
||||
'container' => [
|
||||
'image' => $server->image,
|
||||
// This field is deprecated — use the value in the "build" block.
|
||||
//
|
||||
// TODO: remove this key in V2.
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
'requires_rebuild' => false,
|
||||
],
|
||||
|
@ -162,15 +162,10 @@ class ServerCreationService
|
||||
|
||||
try {
|
||||
$this->daemonServerRepository->setServer($server)->create(
|
||||
array_merge(
|
||||
$this->configurationStructureService->handle($server),
|
||||
[
|
||||
'start_on_completion' => Arr::get($data, 'start_on_completion', false),
|
||||
],
|
||||
),
|
||||
Arr::get($data, 'start_on_completion', false) ?? false
|
||||
);
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
$this->serverDeletionService->withForce(true)->handle($server);
|
||||
$this->serverDeletionService->withForce()->handle($server);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
@ -58,15 +58,20 @@ class SuspensionService
|
||||
throw new ConflictHttpException('Cannot toggle suspension status on a server that is currently being transferred.');
|
||||
}
|
||||
|
||||
$this->connection->transaction(function () use ($action, $server, $isSuspending) {
|
||||
$server->update([
|
||||
'status' => $isSuspending ? Server::STATUS_SUSPENDED : null,
|
||||
]);
|
||||
// Update the server's suspension status.
|
||||
$server->update([
|
||||
'status' => $isSuspending ? Server::STATUS_SUSPENDED : null,
|
||||
]);
|
||||
|
||||
// Only send the suspension request to wings if the server is not currently being transferred.
|
||||
if (is_null($server->transfer)) {
|
||||
$this->daemonServerRepository->setServer($server)->suspend($action === self::ACTION_UNSUSPEND);
|
||||
}
|
||||
});
|
||||
try {
|
||||
// Tell wings to re-sync the server state.
|
||||
$this->daemonServerRepository->setServer($server)->sync();
|
||||
} catch (\Exception $exception) {
|
||||
// Rollback the server's suspension status if wings fails to sync the server.
|
||||
$server->update([
|
||||
'status' => $isSuspending ? null : Server::STATUS_SUSPENDED,
|
||||
]);
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -69,6 +69,7 @@ class ServerTransformer extends BaseTransformer
|
||||
'io' => $server->io,
|
||||
'cpu' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
],
|
||||
'feature_limits' => [
|
||||
'databases' => $server->database_limit,
|
||||
|
@ -55,6 +55,8 @@ class ServerTransformer extends BaseClientTransformer
|
||||
'disk' => $server->disk,
|
||||
'io' => $server->io,
|
||||
'cpu' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
],
|
||||
'invocation' => $service->handle($server, !$this->getUser()->can(Permission::ACTION_STARTUP_READ, $server)),
|
||||
'docker_image' => $server->image,
|
||||
|
@ -28,6 +28,7 @@ class StatsTransformer extends BaseClientTransformer
|
||||
'disk_bytes' => Arr::get($data, 'utilization.disk_bytes', 0),
|
||||
'network_rx_bytes' => Arr::get($data, 'utilization.network.rx_bytes', 0),
|
||||
'network_tx_bytes' => Arr::get($data, 'utilization.network.tx_bytes', 0),
|
||||
'uptime' => Arr::get($data, 'utilization.uptime', 0),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
3
composer.lock
generated
3
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e90dffea56fa4b612863202553cba883",
|
||||
"content-hash": "87961b026d9057d13cfc3f9cb21d367d",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
@ -10949,7 +10949,6 @@
|
||||
"php": "^7.4 || ^8.0",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-pdo_mysql": "*",
|
||||
"ext-zip": "*"
|
||||
},
|
||||
|
@ -141,7 +141,7 @@ return [
|
||||
|
||||
'schedules' => [
|
||||
// The total number of tasks that can exist for any given schedule at once.
|
||||
'per_schedule_task_limit' => 10,
|
||||
'per_schedule_task_limit' => env('PTERODACTYL_PER_SCHEDULE_TASK_LIMIT', 10),
|
||||
],
|
||||
|
||||
'allocations' => [
|
||||
|
@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v1",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2021-07-04T19:18:34-04:00",
|
||||
"exported_at": "2021-11-14T19:23:12+00:00",
|
||||
"name": "Bungeecord",
|
||||
"author": "support@pterodactyl.io",
|
||||
"description": "For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. Whether you are a small server wishing to string multiple game-modes together, or the owner of the ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be able to unlock your community's full potential.",
|
||||
@ -15,14 +15,15 @@
|
||||
"images": [
|
||||
"ghcr.io\/pterodactyl\/yolks:java_8",
|
||||
"ghcr.io\/pterodactyl\/yolks:java_11",
|
||||
"ghcr.io\/pterodactyl\/yolks:java_16"
|
||||
"ghcr.io\/pterodactyl\/yolks:java_16",
|
||||
"ghcr.io\/pterodactyl\/yolks:java_17"
|
||||
],
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}",
|
||||
"config": {
|
||||
"files": "{\r\n \"config.yml\": {\r\n \"parser\": \"yaml\",\r\n \"find\": {\r\n \"listeners[0].query_port\": \"{{server.build.default.port}}\",\r\n \"listeners[0].host\": \"0.0.0.0:{{server.build.default.port}}\",\r\n \"servers.*.address\": {\r\n \"regex:^(127\\\\.0\\\\.0\\\\.1|localhost)(:\\\\d{1,5})?$\": \"{{config.docker.interface}}$2\"\r\n }\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \"Listening on \"\r\n}",
|
||||
"logs": "{\r\n \"custom\": false,\r\n \"location\": \"proxy.log.0\"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "end"
|
||||
},
|
||||
"scripts": {
|
||||
|
File diff suppressed because one or more lines are too long
@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v1",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2021-08-01T03:54:45+03:00",
|
||||
"exported_at": "2021-11-14T19:21:07+00:00",
|
||||
"name": "Paper",
|
||||
"author": "parker@pterodactyl.io",
|
||||
"description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.",
|
||||
@ -15,12 +15,13 @@
|
||||
"images": [
|
||||
"ghcr.io\/pterodactyl\/yolks:java_8",
|
||||
"ghcr.io\/pterodactyl\/yolks:java_11",
|
||||
"ghcr.io\/pterodactyl\/yolks:java_16"
|
||||
"ghcr.io\/pterodactyl\/yolks:java_16",
|
||||
"ghcr.io\/pterodactyl\/yolks:java_17"
|
||||
],
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}",
|
||||
"config": {
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "stop"
|
||||
|
@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v1",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2021-08-01T03:55:24+03:00",
|
||||
"exported_at": "2021-10-22T19:19:17+02:00",
|
||||
"name": "Sponge (SpongeVanilla)",
|
||||
"author": "support@pterodactyl.io",
|
||||
"description": "SpongeVanilla is the SpongeAPI implementation for Vanilla Minecraft.",
|
||||
@ -20,7 +20,7 @@
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}",
|
||||
"config": {
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"enable-query\": \"true\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "stop"
|
||||
|
@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v1",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2021-07-04T19:19:24-04:00",
|
||||
"exported_at": "2021-11-14T19:18:30+00:00",
|
||||
"name": "Vanilla Minecraft",
|
||||
"author": "support@pterodactyl.io",
|
||||
"description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.",
|
||||
@ -15,14 +15,15 @@
|
||||
"images": [
|
||||
"ghcr.io\/pterodactyl\/yolks:java_8",
|
||||
"ghcr.io\/pterodactyl\/yolks:java_11",
|
||||
"ghcr.io\/pterodactyl\/yolks:java_16"
|
||||
"ghcr.io\/pterodactyl\/yolks:java_16",
|
||||
"ghcr.io\/pterodactyl\/yolks:java_17"
|
||||
],
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}",
|
||||
"config": {
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"enable-query\": \"true\",\r\n \"server-port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
|
||||
"logs": "{\r\n \"custom\": false,\r\n \"location\": \"logs\/latest.log\"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "stop"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v1",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2021-05-29T19:02:43-04:00",
|
||||
"exported_at": "2021-09-15T17:07:50-04:00",
|
||||
"name": "Rust",
|
||||
"author": "support@pterodactyl.io",
|
||||
"description": "The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.",
|
||||
@ -13,11 +13,11 @@
|
||||
"quay.io\/pterodactyl\/core:rust"
|
||||
],
|
||||
"file_denylist": [],
|
||||
"startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.logoimage \\\"{{SERVER_LOGO}}\\\" +server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} {{ADDITIONAL_ARGS}}",
|
||||
"startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.logoimage \\\"{{SERVER_LOGO}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s \"+server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\"\" || printf %s \"+server.levelurl {{MAP_URL}}\" ) {{ADDITIONAL_ARGS}}",
|
||||
"config": {
|
||||
"files": "{}",
|
||||
"startup": "{\r\n \"done\": \"Server startup complete\",\r\n \"userInteraction\": []\r\n}",
|
||||
"logs": "{\r\n \"custom\": false,\r\n \"location\": \"latest.log\"\r\n}",
|
||||
"startup": "{\r\n \"done\": \"Server startup complete\"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "quit"
|
||||
},
|
||||
"scripts": {
|
||||
@ -162,6 +162,15 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url"
|
||||
},
|
||||
{
|
||||
"name": "Custom Map URL",
|
||||
"description": "Overwrites the map with the one from the direct download URL. Invalid URLs will cause the server to crash.",
|
||||
"env_variable": "MAP_URL",
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url"
|
||||
}
|
||||
]
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v1",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2021-07-27T14:14:20+03:00",
|
||||
"exported_at": "2021-09-11T14:35:10-04:00",
|
||||
"name": "Ark: Survival Evolved",
|
||||
"author": "dev@shepper.fr",
|
||||
"description": "As a man or woman stranded, naked, freezing, and starving on the unforgiving shores of a mysterious island called ARK, use your skill and cunning to kill or tame and ride the plethora of leviathan dinosaurs and other primeval creatures roaming the land. Hunt, harvest resources, craft items, grow crops, research technologies, and build shelters to withstand the elements and store valuables, all while teaming up with (or preying upon) hundreds of other players to survive, dominate... and escape! \u2014 Gamepedia: ARK",
|
||||
@ -13,7 +13,7 @@
|
||||
"quay.io\/parkervcp\/pterodactyl-images:debian_source"
|
||||
],
|
||||
"file_denylist": [],
|
||||
"startup": "rmv() { echo -e \"stopping server\"; rcon -t rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD} -c saveworld && rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD} -c DoExit; }; trap rmv 15; cd ShooterGame\/Binaries\/Linux && .\/ShooterGameServer {{SERVER_MAP}}?listen?SessionName=\"{{SESSION_NAME}}\"?ServerPassword={{ARK_PASSWORD}}?ServerAdminPassword={{ARK_ADMIN_PASSWORD}}?Port={{SERVER_PORT}}?RCONPort={{RCON_PORT}}?QueryPort={{QUERY_PORT}}?RCONEnabled=True$( [ \"$BATTLE_EYE\" == \"0\" ] || printf %s '?-NoBattlEye' ) -server {{ARGS}} -log & until echo \"waiting for rcon connection...\"; rcon -t rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD}; do sleep 5; done",
|
||||
"startup": "rmv() { echo -e \"stopping server\"; rcon -t rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD} -c saveworld && rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD} -c DoExit; }; trap rmv 15; cd ShooterGame\/Binaries\/Linux && .\/ShooterGameServer {{SERVER_MAP}}?listen?SessionName=\"{{SESSION_NAME}}\"?ServerPassword={{ARK_PASSWORD}}?ServerAdminPassword={{ARK_ADMIN_PASSWORD}}?Port={{SERVER_PORT}}?RCONPort={{RCON_PORT}}?QueryPort={{QUERY_PORT}}?RCONEnabled=True$( [ \"$BATTLE_EYE\" == \"1\" ] || printf %s ' -NoBattlEye' ) -server {{ARGS}} -log & until echo \"waiting for rcon connection...\"; rcon -t rcon -a 127.0.0.1:${RCON_PORT} -p ${ARK_ADMIN_PASSWORD}; do sleep 5; done",
|
||||
"config": {
|
||||
"files": "{}",
|
||||
"startup": "{\r\n \"done\": \"Waiting commands for 127.0.0.1:\"\r\n}",
|
||||
@ -48,7 +48,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Server Map",
|
||||
"description": "Available Maps: TheIsland, TheCenter, Ragnarok, ScorchedEarth_P, Aberration_P, Extinction, Valguero_P, Genesis, CrystalIsles, Gen2",
|
||||
"description": "Available Maps: TheIsland, TheCenter, Ragnarok, ScorchedEarth_P, Aberration_P, Extinction, Valguero_P, Genesis, CrystalIsles, Gen2, LostIsland",
|
||||
"env_variable": "SERVER_MAP",
|
||||
"default_value": "TheIsland",
|
||||
"user_viewable": true,
|
||||
|
@ -4,11 +4,13 @@
|
||||
"version": "PTDL_v1",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2021-06-05T16:19:30-04:00",
|
||||
"exported_at": "2021-09-10T14:36:37-04:00",
|
||||
"name": "Counter-Strike: Global Offensive",
|
||||
"author": "support@pterodactyl.io",
|
||||
"description": "Counter-Strike: Global Offensive is a multiplayer first-person shooter video game developed by Hidden Path Entertainment and Valve Corporation.",
|
||||
"features": null,
|
||||
"features": [
|
||||
"gsl_token"
|
||||
],
|
||||
"images": [
|
||||
"ghcr.io\/pterodactyl\/games:source"
|
||||
],
|
||||
@ -16,19 +18,18 @@
|
||||
"startup": ".\/srcds_run -game csgo -console -port {{SERVER_PORT}} +ip 0.0.0.0 +map {{SRCDS_MAP}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}",
|
||||
"config": {
|
||||
"files": "{}",
|
||||
"startup": "{\r\n \"done\": \"Connection to Steam servers successful\",\r\n \"userInteraction\": []\r\n}",
|
||||
"logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/latest.log\"\r\n}",
|
||||
"startup": "{\r\n \"done\": \"Connection to Steam servers successful\"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "quit"
|
||||
},
|
||||
"scripts": {
|
||||
"installation": {
|
||||
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'ubuntu:18.04'\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
|
||||
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
|
||||
"container": "ghcr.io\/pterodactyl\/installers:debian",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"variables": [{
|
||||
"name": "Map",
|
||||
"description": "The default map for the server.",
|
||||
"env_variable": "SRCDS_MAP",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v1",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2021-06-05T16:24:05-04:00",
|
||||
"exported_at": "2021-09-10T14:36:22-04:00",
|
||||
"name": "Custom Source Engine Game",
|
||||
"author": "support@pterodactyl.io",
|
||||
"description": "This option allows modifying the startup arguments and other details to run a custom SRCDS based game on the panel.",
|
||||
@ -16,19 +16,18 @@
|
||||
"startup": ".\/srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart",
|
||||
"config": {
|
||||
"files": "{}",
|
||||
"startup": "{\r\n \"done\": \"gameserver Steam ID\",\r\n \"userInteraction\": []\r\n}",
|
||||
"logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/latest.log\"\r\n}",
|
||||
"startup": "{\r\n \"done\": \"gameserver Steam ID\"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "quit"
|
||||
},
|
||||
"scripts": {
|
||||
"installation": {
|
||||
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'debian:buster-slim'\r\n\r\n##\r\n#\r\n# Variables\r\n# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.\r\n# WINDOWS_INSTALL - if it's a windows server you want to install set to 1\r\n# SRCDS_APPID - steam app id ffound here - https:\/\/developer.valvesoftware.com\/wiki\/Dedicated_Servers_List\r\n# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.\r\n#\r\n##\r\n\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
|
||||
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n##\r\n#\r\n# Variables\r\n# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.\r\n# WINDOWS_INSTALL - if it's a windows server you want to install set to 1\r\n# SRCDS_APPID - steam app id ffound here - https:\/\/developer.valvesoftware.com\/wiki\/Dedicated_Servers_List\r\n# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.\r\n#\r\n##\r\n\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
|
||||
"container": "ghcr.io\/pterodactyl\/installers:debian",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"variables": [{
|
||||
"name": "Game ID",
|
||||
"description": "The ID corresponding to the game to download and run using SRCDS.",
|
||||
"env_variable": "SRCDS_APPID",
|
||||
|
@ -4,25 +4,27 @@
|
||||
"version": "PTDL_v1",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2021-06-05T16:28:06-04:00",
|
||||
"exported_at": "2021-12-04T18:47:10+00:00",
|
||||
"name": "Garrys Mod",
|
||||
"author": "support@pterodactyl.io",
|
||||
"description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.",
|
||||
"features": null,
|
||||
"features": [
|
||||
"gsl_token"
|
||||
],
|
||||
"images": [
|
||||
"ghcr.io\/pterodactyl\/games:source"
|
||||
],
|
||||
"file_denylist": [],
|
||||
"startup": ".\/srcds_run -game garrysmod -console -port {{SERVER_PORT}} +ip 0.0.0.0 +host_workshop_collection {{WORKSHOP_ID}} +map {{SRCDS_MAP}} +gamemode {{GAMEMODE}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}} +maxplayers {{MAX_PLAYERS}} -tickrate {{TICKRATE}}",
|
||||
"startup": ".\/srcds_run -game garrysmod -console -port {{SERVER_PORT}} +ip 0.0.0.0 +host_workshop_collection {{WORKSHOP_ID}} +map {{SRCDS_MAP}} +gamemode {{GAMEMODE}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}} +maxplayers {{MAX_PLAYERS}} -tickrate {{TICKRATE}} $( [ \"$LUA_REFRESH\" == \"1\" ] || printf %s '-disableluarefresh' )",
|
||||
"config": {
|
||||
"files": "{}",
|
||||
"startup": "{\r\n \"done\": \"gameserver Steam ID\",\r\n \"userInteraction\": []\r\n}",
|
||||
"logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/latest.log\"\r\n}",
|
||||
"startup": "{\r\n \"done\": \"gameserver Steam ID\"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "quit"
|
||||
},
|
||||
"scripts": {
|
||||
"installation": {
|
||||
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'debian:buster-slim'\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so\r\n\r\n# Creating needed default files for the game\r\ncd \/mnt\/server\/garrysmod\/lua\/autorun\/server\r\necho '\r\n-- Docs: https:\/\/wiki.garrysmod.com\/page\/resource\/AddWorkshop\r\n-- Place the ID of the workshop addon you want to be downloaded to people who join your server, not the collection ID\r\n-- Use https:\/\/beta.configcreator.com\/create\/gmod\/resources.lua to easily create a list based on your collection ID\r\n\r\nresource.AddWorkshop( \"\" )\r\n' > workshop.lua\r\n\r\ncd \/mnt\/server\/garrysmod\/cfg\r\necho '\r\n\/\/ Please do not set RCon in here, use the startup parameters.\r\n\r\nhostname\t\t\"New Gmod Server\"\r\nsv_password\t\t\"\"\r\nsv_loadingurl \"\"\r\n\r\n\/\/ Steam Server List Settings\r\nsv_region \"255\"\r\nsv_lan \"0\"\r\nsv_max_queries_sec_global \"30000\"\r\nsv_max_queries_window \"45\"\r\nsv_max_queries_sec \"5\"\r\n\r\n\/\/ Server Limits\r\nsbox_maxprops\t\t100\r\nsbox_maxragdolls\t5\r\nsbox_maxnpcs\t\t10\r\nsbox_maxballoons\t10\r\nsbox_maxeffects\t\t10\r\nsbox_maxdynamite\t10\r\nsbox_maxlamps\t\t10\r\nsbox_maxthrusters\t10\r\nsbox_maxwheels\t\t10\r\nsbox_maxhoverballs\t10\r\nsbox_maxvehicles\t20\r\nsbox_maxbuttons\t\t10\r\nsbox_maxsents\t\t20\r\nsbox_maxemitters\t5\r\nsbox_godmode\t\t0\r\nsbox_noclip\t\t 0\r\n\r\n\/\/ Network Settings - Please keep these set to default.\r\n\r\nsv_minrate\t\t75000\r\nsv_maxrate\t\t0\r\ngmod_physiterations\t2\r\nnet_splitpacket_maxrate\t45000\r\ndecalfrequency\t\t12 \r\n\r\n\/\/ Execute Ban Files - Please do not edit\r\nexec banned_ip.cfg \r\nexec banned_user.cfg \r\n\r\n\/\/ Add custom lines under here\r\n' > server.cfg",
|
||||
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +force_install_dir \/mnt\/server +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so\r\n\r\n# Creating needed default files for the game\r\ncd \/mnt\/server\/garrysmod\/lua\/autorun\/server\r\necho '\r\n-- Docs: https:\/\/wiki.garrysmod.com\/page\/resource\/AddWorkshop\r\n-- Place the ID of the workshop addon you want to be downloaded to people who join your server, not the collection ID\r\n-- Use https:\/\/beta.configcreator.com\/create\/gmod\/resources.lua to easily create a list based on your collection ID\r\n\r\nresource.AddWorkshop( \"\" )\r\n' > workshop.lua\r\n\r\ncd \/mnt\/server\/garrysmod\/cfg\r\necho '\r\n\/\/ Please do not set RCon in here, use the startup parameters.\r\n\r\nhostname\t\t\"New Gmod Server\"\r\nsv_password\t\t\"\"\r\nsv_loadingurl \"\"\r\nsv_downloadurl \"\"\r\n\r\n\/\/ Steam Server List Settings\r\n\/\/ sv_location \"eu\"\r\nsv_region \"255\"\r\nsv_lan \"0\"\r\nsv_max_queries_sec_global \"30000\"\r\nsv_max_queries_window \"45\"\r\nsv_max_queries_sec \"5\"\r\n\r\n\/\/ Server Limits\r\nsbox_maxprops\t\t100\r\nsbox_maxragdolls\t5\r\nsbox_maxnpcs\t\t10\r\nsbox_maxballoons\t10\r\nsbox_maxeffects\t\t10\r\nsbox_maxdynamite\t10\r\nsbox_maxlamps\t\t10\r\nsbox_maxthrusters\t10\r\nsbox_maxwheels\t\t10\r\nsbox_maxhoverballs\t10\r\nsbox_maxvehicles\t20\r\nsbox_maxbuttons\t\t10\r\nsbox_maxsents\t\t20\r\nsbox_maxemitters\t5\r\nsbox_godmode\t\t0\r\nsbox_noclip\t\t 0\r\n\r\n\/\/ Network Settings - Please keep these set to default.\r\n\r\nsv_minrate\t\t75000\r\nsv_maxrate\t\t0\r\ngmod_physiterations\t2\r\nnet_splitpacket_maxrate\t45000\r\ndecalfrequency\t\t12 \r\n\r\n\/\/ Execute Ban Files - Please do not edit\r\nexec banned_ip.cfg \r\nexec banned_user.cfg \r\n\r\n\/\/ Add custom lines under here\r\n' > server.cfg",
|
||||
"container": "ghcr.io\/pterodactyl\/installers:debian",
|
||||
"entrypoint": "bash"
|
||||
}
|
||||
@ -90,6 +92,15 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer|max:100"
|
||||
},
|
||||
{
|
||||
"name": "Lua Refresh",
|
||||
"description": "0 = disable Lua refresh,\r\n1 = enable Lua refresh",
|
||||
"env_variable": "LUA_REFRESH",
|
||||
"default_value": "0",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|boolean"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddForeignKeysToMountNodeTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// Fix the columns having a different type than their relations.
|
||||
Schema::table('mount_node', function (Blueprint $table) {
|
||||
$table->unsignedInteger('node_id')->change();
|
||||
$table->unsignedInteger('mount_id')->change();
|
||||
});
|
||||
|
||||
// Fetch an array of node and mount ids to check relations against.
|
||||
$nodes = DB::table('nodes')->select('id')->pluck('id')->toArray();
|
||||
$mounts = DB::table('mounts')->select('id')->pluck('id')->toArray();
|
||||
|
||||
// Drop any relations that are missing a node or mount.
|
||||
DB::table('mount_node')
|
||||
->select('node_id', 'mount_id')
|
||||
->whereNotIn('node_id', $nodes)
|
||||
->orWhereNotIn('mount_id', $mounts)
|
||||
->delete();
|
||||
|
||||
Schema::table('mount_node', function (Blueprint $table) {
|
||||
$table->foreign('node_id')
|
||||
->references('id')
|
||||
->on('nodes')
|
||||
->cascadeOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->foreign('mount_id')->references('id')
|
||||
->on('mounts')
|
||||
->cascadeOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('mount_node', function (Blueprint $table) {
|
||||
$table->dropForeign(['node_id']);
|
||||
$table->dropForeign(['mount_id']);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddForeignKeysToMountServerTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// Fix the columns having a different type than their relations.
|
||||
Schema::table('mount_server', function (Blueprint $table) {
|
||||
$table->unsignedInteger('server_id')->change();
|
||||
$table->unsignedInteger('mount_id')->change();
|
||||
});
|
||||
|
||||
// Fetch an array of node and mount ids to check relations against.
|
||||
$servers = DB::table('servers')->select('id')->pluck('id')->toArray();
|
||||
$mounts = DB::table('mounts')->select('id')->pluck('id')->toArray();
|
||||
|
||||
// Drop any relations that are missing a server or mount.
|
||||
DB::table('mount_server')
|
||||
->select('server_id', 'mount_id')
|
||||
->whereNotIn('server_id', $servers)
|
||||
->orWhereNotIn('mount_id', $mounts)
|
||||
->delete();
|
||||
|
||||
Schema::table('mount_server', function (Blueprint $table) {
|
||||
$table->foreign('server_id')
|
||||
->references('id')
|
||||
->on('servers')
|
||||
->cascadeOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->foreign('mount_id')->references('id')
|
||||
->on('mounts')
|
||||
->cascadeOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('mount_server', function (Blueprint $table) {
|
||||
$table->dropForeign(['server_id']);
|
||||
$table->dropForeign(['mount_id']);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddForeignKeysToEggMountTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// Fix the columns having a different type than their relations.
|
||||
Schema::table('egg_mount', function (Blueprint $table) {
|
||||
$table->unsignedInteger('egg_id')->change();
|
||||
$table->unsignedInteger('mount_id')->change();
|
||||
});
|
||||
|
||||
// Fetch an array of node and mount ids to check relations against.
|
||||
$eggs = DB::table('eggs')->select('id')->pluck('id')->toArray();
|
||||
$mounts = DB::table('mounts')->select('id')->pluck('id')->toArray();
|
||||
|
||||
// Drop any relations that are missing an egg or mount.
|
||||
DB::table('egg_mount')
|
||||
->select('egg_id', 'mount_id')
|
||||
->whereNotIn('egg_id', $eggs)
|
||||
->orWhereNotIn('mount_id', $mounts)
|
||||
->delete();
|
||||
|
||||
Schema::table('egg_mount', function (Blueprint $table) {
|
||||
$table->foreign('egg_id')
|
||||
->references('id')
|
||||
->on('eggs')
|
||||
->cascadeOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->foreign('mount_id')->references('id')
|
||||
->on('mounts')
|
||||
->cascadeOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('egg_mount', function (Blueprint $table) {
|
||||
$table->dropForeign(['egg_id']);
|
||||
$table->dropForeign(['mount_id']);
|
||||
});
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ x-common:
|
||||
#
|
||||
services:
|
||||
database:
|
||||
image: library/mysql:8.0
|
||||
image: mariadb:10.5
|
||||
restart: always
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
volumes:
|
||||
@ -57,7 +57,7 @@ services:
|
||||
- cache
|
||||
volumes:
|
||||
- "/srv/pterodactyl/var/:/app/var/"
|
||||
- "/srv/pterodactyl/nginx/:/etc/nginx/conf.d/"
|
||||
- "/srv/pterodactyl/nginx/:/etc/nginx/http.d/"
|
||||
- "/srv/pterodactyl/certs/:/etc/letsencrypt/"
|
||||
- "/srv/pterodactyl/logs/:/app/storage/logs"
|
||||
environment:
|
||||
|
@ -37,7 +37,7 @@
|
||||
"swr": "^0.2.3",
|
||||
"tailwindcss": "^2.0.2",
|
||||
"uuid": "^3.3.2",
|
||||
"xterm": "^4.12.0",
|
||||
"xterm": "^4.15.0",
|
||||
"xterm-addon-attach": "^0.6.0",
|
||||
"xterm-addon-fit": "^0.4.0",
|
||||
"xterm-addon-search": "^0.7.0",
|
||||
|
@ -7,10 +7,21 @@ const http: AxiosInstance = axios.create({
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': (window as any).X_CSRF_TOKEN as string || '',
|
||||
},
|
||||
});
|
||||
|
||||
http.interceptors.request.use(req => {
|
||||
const cookies = document.cookie.split(';').reduce((obj, val) => {
|
||||
const [ key, value ] = val.trim().split('=').map(decodeURIComponent);
|
||||
|
||||
return { ...obj, [key]: value };
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
req.headers['X-XSRF-TOKEN'] = cookies['XSRF-TOKEN'] || 'nil';
|
||||
|
||||
return req;
|
||||
});
|
||||
|
||||
http.interceptors.request.use(req => {
|
||||
if (!req.url?.endsWith('/resources') && (req.url?.indexOf('_debugbar') || -1) < 0) {
|
||||
store.getActions().progress.startContinuous();
|
||||
|
@ -10,6 +10,7 @@ export interface ServerStats {
|
||||
diskUsageInBytes: number;
|
||||
networkRxInBytes: number;
|
||||
networkTxInBytes: number;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
export default (server: string): Promise<ServerStats> => {
|
||||
@ -23,6 +24,7 @@ export default (server: string): Promise<ServerStats> => {
|
||||
diskUsageInBytes: attributes.resources.disk_bytes,
|
||||
networkRxInBytes: attributes.resources.network_rx_bytes,
|
||||
networkTxInBytes: attributes.resources.network_tx_bytes,
|
||||
uptime: attributes.resources.uptime,
|
||||
}))
|
||||
.catch(reject);
|
||||
});
|
||||
|
@ -7,6 +7,9 @@ import { ApplicationStore } from '@/state';
|
||||
import SearchContainer from '@/components/dashboard/search/SearchContainer';
|
||||
import tw, { theme } from 'twin.macro';
|
||||
import styled from 'styled-components/macro';
|
||||
import http from '@/api/http';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Navigation = styled.div`
|
||||
${tw`w-full bg-neutral-900 shadow-md overflow-x-auto`};
|
||||
@ -27,7 +30,7 @@ const Navigation = styled.div`
|
||||
const RightNavigation = styled.div`
|
||||
${tw`flex h-full items-center justify-center`};
|
||||
|
||||
& > a, & > .navigation-link {
|
||||
& > a, & > button, & > .navigation-link {
|
||||
${tw`flex items-center h-full no-underline text-neutral-300 px-6 cursor-pointer transition-all duration-150`};
|
||||
|
||||
&:active, &:hover {
|
||||
@ -43,9 +46,19 @@ const RightNavigation = styled.div`
|
||||
export default () => {
|
||||
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
||||
const rootAdmin = useStoreState((state: ApplicationStore) => state.user.data!.rootAdmin);
|
||||
const [ isLoggingOut, setIsLoggingOut ] = useState(false);
|
||||
|
||||
const onTriggerLogout = () => {
|
||||
setIsLoggingOut(true);
|
||||
http.post('/auth/logout').finally(() => {
|
||||
// @ts-ignore
|
||||
window.location = '/';
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Navigation>
|
||||
<SpinnerOverlay visible={isLoggingOut} />
|
||||
<div css={tw`mx-auto w-full flex items-center`} style={{ maxWidth: '1200px', height: '3.5rem' }}>
|
||||
<div id={'logo'}>
|
||||
<Link to={'/'}>
|
||||
@ -65,9 +78,9 @@ export default () => {
|
||||
<FontAwesomeIcon icon={faCogs}/>
|
||||
</a>
|
||||
}
|
||||
<a href={'/auth/logout'}>
|
||||
<button onClick={onTriggerLogout}>
|
||||
<FontAwesomeIcon icon={faSignOutAlt}/>
|
||||
</a>
|
||||
</button>
|
||||
</RightNavigation>
|
||||
</div>
|
||||
</Navigation>
|
||||
|
@ -47,7 +47,7 @@ export default () => {
|
||||
>
|
||||
<UpdateEmailAddressForm/>
|
||||
</ContentBox>
|
||||
<ContentBox css={tw`xl:ml-8 mt-8 xl:mt-0`} title={'Configure Two Factor'}>
|
||||
<ContentBox css={tw`lg:ml-8 mt-8 lg:mt-0`} title={'Configure Two Factor'}>
|
||||
<ConfigureTwoFactorForm/>
|
||||
</ContentBox>
|
||||
</Container>
|
||||
|
@ -4,7 +4,7 @@ import { faEthernet, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Server } from '@/api/server/getServer';
|
||||
import getServerResourceUsage, { ServerPowerState, ServerStats } from '@/api/server/getServerResourceUsage';
|
||||
import { bytesToHuman, megabytesToHuman } from '@/helpers';
|
||||
import { bytesToHuman, megabytesToHuman, formatIp } from '@/helpers';
|
||||
import tw from 'twin.macro';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
@ -97,7 +97,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
||||
{
|
||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
||||
<React.Fragment key={allocation.ip + allocation.port.toString()}>
|
||||
{allocation.alias || allocation.ip}:{allocation.port}
|
||||
{allocation.alias || formatIp(allocation.ip)}:{allocation.port}
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import tw from 'twin.macro';
|
||||
import Input from '@/components/elements/Input';
|
||||
|
||||
import { formatIp } from '@/helpers';
|
||||
type Props = RequiredModalProps;
|
||||
|
||||
interface Values {
|
||||
@ -109,7 +109,7 @@ export default ({ ...props }: Props) => {
|
||||
<p css={tw`mt-1 text-xs text-neutral-400`}>
|
||||
{
|
||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
||||
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
|
||||
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || formatIp(allocation.ip)}:{allocation.port}</span>
|
||||
))
|
||||
}
|
||||
</p>
|
||||
|
@ -1,16 +1,16 @@
|
||||
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { SocketEvent } from '@/components/server/events';
|
||||
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
|
||||
import { mutate } from 'swr';
|
||||
import { getDirectorySwrKey } from '@/plugins/useFileManagerSwr';
|
||||
|
||||
const InstallListener = () => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
|
||||
const { mutate } = useFileManagerSwr();
|
||||
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
|
||||
|
||||
useWebsocketEvent(SocketEvent.BACKUP_RESTORE_COMPLETED, () => {
|
||||
mutate(undefined);
|
||||
mutate(getDirectorySwrKey(uuid, '/'), undefined);
|
||||
setServerFromState(s => ({ ...s, status: null }));
|
||||
});
|
||||
|
||||
|
@ -7,7 +7,7 @@ import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
import ServerDetailsBlock from '@/components/server/ServerDetailsBlock';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import PowerControls from '@/components/server/PowerControls';
|
||||
import { EulaModalFeature, JavaVersionModalFeature } from '@feature/index';
|
||||
import { EulaModalFeature, JavaVersionModalFeature, GSLTokenModalFeature } from '@feature/index';
|
||||
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
|
||||
@ -60,6 +60,7 @@ const ServerConsole = () => {
|
||||
<React.Suspense fallback={null}>
|
||||
{eggFeatures.includes('eula') && <EulaModalFeature/>}
|
||||
{eggFeatures.includes('java_version') && <JavaVersionModalFeature/>}
|
||||
{eggFeatures.includes('gsl_token') && <GSLTokenModalFeature/>}
|
||||
</React.Suspense>
|
||||
</div>
|
||||
</ServerContentBlock>
|
||||
|
@ -2,19 +2,21 @@ import React, { useEffect, useState } from 'react';
|
||||
import tw, { TwStyle } from 'twin.macro';
|
||||
import { faCircle, faEthernet, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { bytesToHuman, megabytesToHuman } from '@/helpers';
|
||||
import { bytesToHuman, megabytesToHuman, formatIp } from '@/helpers';
|
||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import { SocketEvent, SocketRequest } from '@/components/server/events';
|
||||
import UptimeDuration from '@/components/server/UptimeDuration';
|
||||
|
||||
interface Stats {
|
||||
memory: number;
|
||||
cpu: number;
|
||||
disk: number;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
function statusToColor (status: string|null, installing: boolean): TwStyle {
|
||||
function statusToColor (status: string | null, installing: boolean): TwStyle {
|
||||
if (installing) {
|
||||
status = '';
|
||||
}
|
||||
@ -30,7 +32,7 @@ function statusToColor (status: string|null, installing: boolean): TwStyle {
|
||||
}
|
||||
|
||||
const ServerDetailsBlock = () => {
|
||||
const [ stats, setStats ] = useState<Stats>({ memory: 0, cpu: 0, disk: 0 });
|
||||
const [ stats, setStats ] = useState<Stats>({ memory: 0, cpu: 0, disk: 0, uptime: 0 });
|
||||
|
||||
const status = ServerContext.useStoreState(state => state.status.value);
|
||||
const connected = ServerContext.useStoreState(state => state.socket.connected);
|
||||
@ -48,6 +50,7 @@ const ServerDetailsBlock = () => {
|
||||
memory: stats.memory_bytes,
|
||||
cpu: stats.cpu_absolute,
|
||||
disk: stats.disk_bytes,
|
||||
uptime: stats.uptime || 0,
|
||||
});
|
||||
};
|
||||
|
||||
@ -69,7 +72,7 @@ const ServerDetailsBlock = () => {
|
||||
const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring);
|
||||
const limits = ServerContext.useStoreState(state => state.server.data!.limits);
|
||||
const primaryAllocation = ServerContext.useStoreState(state => state.server.data!.allocations.filter(alloc => alloc.isDefault).map(
|
||||
allocation => (allocation.alias || allocation.ip) + ':' + allocation.port
|
||||
allocation => (allocation.alias || formatIp(allocation.ip)) + ':' + allocation.port,
|
||||
)).toString();
|
||||
|
||||
const diskLimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited';
|
||||
@ -88,6 +91,11 @@ const ServerDetailsBlock = () => {
|
||||
]}
|
||||
/>
|
||||
{!status ? 'Connecting...' : (isInstalling ? 'Installing' : (isTransferring) ? 'Transferring' : status)}
|
||||
{stats.uptime > 0 &&
|
||||
<span css={tw`ml-2 lowercase`}>
|
||||
(<UptimeDuration uptime={stats.uptime / 1000}/>)
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
<CopyOnClick text={primaryAllocation}>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
|
@ -97,6 +97,7 @@ export default () => {
|
||||
setCpu(
|
||||
new Chart(node.getContext('2d')!, chartDefaults({
|
||||
callback: (value) => `${value}% `,
|
||||
suggestedMax: limits.cpu,
|
||||
})),
|
||||
);
|
||||
}, []);
|
||||
|
15
resources/scripts/components/server/UptimeDuration.tsx
Normal file
15
resources/scripts/components/server/UptimeDuration.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
export default ({ uptime }: { uptime: number }) => {
|
||||
const days = Math.floor(uptime / (24 * 60 * 60));
|
||||
const hours = Math.floor(Math.floor(uptime) / 60 / 60 % 24);
|
||||
const remainder = Math.floor(uptime - (hours * 60 * 60));
|
||||
const minutes = Math.floor(remainder / 60 % 60);
|
||||
const seconds = remainder % 60;
|
||||
|
||||
if (days > 0) {
|
||||
return <>{days}d {hours}h {minutes}m</>;
|
||||
}
|
||||
|
||||
return <>{hours}h {minutes}m {seconds}s</>;
|
||||
};
|
@ -76,7 +76,7 @@ export default ({ database, className }: Props) => {
|
||||
<FlashMessageRender byKey={'database:delete'} css={tw`mb-6`}/>
|
||||
<h2 css={tw`text-2xl mb-6`}>Confirm database deletion</h2>
|
||||
<p css={tw`text-sm`}>
|
||||
Deleting a database is a permanent action, it cannot be undone. This will permanetly
|
||||
Deleting a database is a permanent action, it cannot be undone. This will permanently
|
||||
delete the <strong>{database.name}</strong> database and remove all associated data.
|
||||
</p>
|
||||
<Form css={tw`m-0 mt-6`}>
|
||||
|
@ -0,0 +1,101 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import Modal from '@/components/elements/Modal';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { SocketEvent, SocketRequest } from '@/components/server/events';
|
||||
import Field from '@/components/elements/Field';
|
||||
import updateStartupVariable from '@/api/server/updateStartupVariable';
|
||||
import { Form, Formik } from 'formik';
|
||||
|
||||
interface Values {
|
||||
gslToken: string;
|
||||
}
|
||||
|
||||
const GSLTokenModalFeature = () => {
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const status = ServerContext.useStoreState(state => state.status.value);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected || !instance || status === 'running') return;
|
||||
|
||||
const errors = [
|
||||
'(gsl token expired)',
|
||||
'(account not found)',
|
||||
];
|
||||
|
||||
const listener = (line: string) => {
|
||||
if (errors.some(p => line.toLowerCase().includes(p))) {
|
||||
setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
instance.addListener(SocketEvent.CONSOLE_OUTPUT, listener);
|
||||
|
||||
return () => {
|
||||
instance.removeListener(SocketEvent.CONSOLE_OUTPUT, listener);
|
||||
};
|
||||
}, [ connected, instance, status ]);
|
||||
|
||||
const updateGSLToken = (values: Values) => {
|
||||
setLoading(true);
|
||||
clearFlashes('feature:gslToken');
|
||||
|
||||
updateStartupVariable(uuid, 'STEAM_ACC', values.gslToken)
|
||||
.then(() => {
|
||||
if (instance) {
|
||||
instance.send(SocketRequest.SET_STATE, 'restart');
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'feature:gslToken', error });
|
||||
})
|
||||
.then(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes('feature:gslToken');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
onSubmit={updateGSLToken}
|
||||
initialValues={{ gslToken: '' }}
|
||||
>
|
||||
<Modal visible={visible} onDismissed={() => setVisible(false)} closeOnBackground={false} showSpinnerOverlay={loading}>
|
||||
<FlashMessageRender key={'feature:gslToken'} css={tw`mb-4`}/>
|
||||
<Form>
|
||||
<h2 css={tw`text-2xl mb-4 text-neutral-100`}>Invalid GSL token!</h2>
|
||||
<p css={tw`mt-4`}>It seems like your Gameserver Login Token (GSL token) is invalid or has expired.</p>
|
||||
<p css={tw`mt-4`}>You can either generate a new one and enter it below or leave the field blank to remove it completely.</p>
|
||||
<div css={tw`sm:flex items-center mt-4`}>
|
||||
<Field
|
||||
name={'gslToken'}
|
||||
label={'GSL Token'}
|
||||
description={'Visit https://steamcommunity.com/dev/managegameservers to generate a token.'}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-8 sm:flex items-center justify-end`}>
|
||||
<Button type={'submit'} css={tw`mt-4 sm:mt-0 sm:ml-4 w-full sm:w-auto`}>
|
||||
Update GSL Token
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default GSLTokenModalFeature;
|
@ -10,15 +10,16 @@ import { SocketEvent, SocketRequest } from '@/components/server/events';
|
||||
import Select from '@/components/elements/Select';
|
||||
|
||||
const dockerImageList = [
|
||||
{ name: 'Java 8', image: 'ghcr.io/pterodactyl/yolks:java_8' },
|
||||
{ name: 'Java 11', image: 'ghcr.io/pterodactyl/yolks:java_11' },
|
||||
{ name: 'Java 17', image: 'ghcr.io/pterodactyl/yolks:java_17' },
|
||||
{ name: 'Java 16', image: 'ghcr.io/pterodactyl/yolks:java_16' },
|
||||
{ name: 'Java 11', image: 'ghcr.io/pterodactyl/yolks:java_11' },
|
||||
{ name: 'Java 8', image: 'ghcr.io/pterodactyl/yolks:java_8' },
|
||||
];
|
||||
|
||||
const JavaVersionModalFeature = () => {
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const [ selectedVersion, setSelectedVersion ] = useState('ghcr.io/pterodactyl/yolks:java_16');
|
||||
const [ selectedVersion, setSelectedVersion ] = useState('ghcr.io/pterodactyl/yolks:java_17');
|
||||
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const status = ServerContext.useStoreState(state => state.status.value);
|
||||
@ -30,6 +31,7 @@ const JavaVersionModalFeature = () => {
|
||||
|
||||
const errors = [
|
||||
'minecraft 1.17 requires running the server with java 16 or above',
|
||||
'minecraft 1.18 requires running the server with java 17 or above',
|
||||
'java.lang.unsupportedclassversionerror',
|
||||
'unsupported major.minor version',
|
||||
'has been compiled by a more recent version of the java runtime',
|
||||
@ -75,9 +77,9 @@ const JavaVersionModalFeature = () => {
|
||||
return (
|
||||
<Modal visible={visible} onDismissed={() => setVisible(false)} closeOnBackground={false} showSpinnerOverlay={loading}>
|
||||
<FlashMessageRender key={'feature:javaVersion'} css={tw`mb-4`}/>
|
||||
<h2 css={tw`text-2xl mb-4 text-neutral-100`}>Invalid Java Version, Update Docker Image?</h2>
|
||||
<p css={tw`mt-4`}>This server is unable to start due to the required java version not being met.</p>
|
||||
<p css={tw`mt-4`}>By pressing {'"Update Docker Image"'} below you are acknowledging that the docker image this server uses will be changed to a image below that has the Java version you are requesting.</p>
|
||||
<h2 css={tw`text-2xl mb-4 text-neutral-100`}>Invalid Java version, update Docker image?</h2>
|
||||
<p css={tw`mt-4`}>This server is unable to start due to the required Java version not being met.</p>
|
||||
<p css={tw`mt-4`}>By pressing {'"Update Docker Image"'} below you are acknowledging that the Docker image this server uses will be changed to an image below that has the Java version you are requesting.</p>
|
||||
<div css={tw`sm:flex items-center mt-4`}>
|
||||
<p>Please select a Java version from the list below.</p>
|
||||
<Select
|
||||
|
@ -8,5 +8,6 @@ import { lazy } from 'react';
|
||||
*/
|
||||
const EulaModalFeature = lazy(() => import(/* webpackChunkName: "feature.eula" */'@feature/eula/EulaModalFeature'));
|
||||
const JavaVersionModalFeature = lazy(() => import(/* webpackChunkName: "feature.java_version" */'@feature/JavaVersionModalFeature'));
|
||||
const GSLTokenModalFeature = lazy(() => import(/* webpackChunkName: "feature.gsl_token" */'@feature/GSLTokenModalFeature'));
|
||||
|
||||
export { EulaModalFeature, JavaVersionModalFeature };
|
||||
export { EulaModalFeature, JavaVersionModalFeature, GSLTokenModalFeature };
|
||||
|
@ -18,6 +18,7 @@ import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import DeleteAllocationButton from '@/components/server/network/DeleteAllocationButton';
|
||||
import setPrimaryServerAllocation from '@/api/server/network/setPrimaryServerAllocation';
|
||||
import getServerAllocations from '@/api/swr/getServerAllocations';
|
||||
import { formatIp } from '@/helpers';
|
||||
|
||||
const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm inline-block`}`;
|
||||
const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
|
||||
@ -66,7 +67,7 @@ const AllocationRow = ({ allocation }: Props) => {
|
||||
<div css={tw`mr-4 flex-1 md:w-40`}>
|
||||
{allocation.alias ?
|
||||
<CopyOnClick text={allocation.alias}><Code css={tw`w-40 truncate`}>{allocation.alias}</Code></CopyOnClick> :
|
||||
<CopyOnClick text={allocation.ip}><Code>{allocation.ip}</Code></CopyOnClick>}
|
||||
<CopyOnClick text={formatIp(allocation.ip)}><Code>{formatIp(allocation.ip)}</Code></CopyOnClick>}
|
||||
<Label>{allocation.alias ? 'Hostname' : 'IP Address'}</Label>
|
||||
</div>
|
||||
<div css={tw`w-16 md:w-24 overflow-hidden`}>
|
||||
|
@ -13,6 +13,7 @@ import { LinkButton } from '@/components/elements/Button';
|
||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import { formatIp } from '@/helpers';
|
||||
|
||||
export default () => {
|
||||
const username = useStoreState(state => state.user.data!.username);
|
||||
@ -30,10 +31,10 @@ export default () => {
|
||||
<TitledGreyBox title={'SFTP Details'} css={tw`mb-6 md:mb-10`}>
|
||||
<div>
|
||||
<Label>Server Address</Label>
|
||||
<CopyOnClick text={`sftp://${sftp.ip}:${sftp.port}`}>
|
||||
<CopyOnClick text={`sftp://${formatIp(sftp.ip)}:${sftp.port}`}>
|
||||
<Input
|
||||
type={'text'}
|
||||
value={`sftp://${sftp.ip}:${sftp.port}`}
|
||||
value={`sftp://${formatIp(sftp.ip)}:${sftp.port}`}
|
||||
readOnly
|
||||
/>
|
||||
</CopyOnClick>
|
||||
@ -59,7 +60,7 @@ export default () => {
|
||||
<div css={tw`ml-4`}>
|
||||
<LinkButton
|
||||
isSecondary
|
||||
href={`sftp://${username}.${id}@${sftp.ip}:${sftp.port}`}
|
||||
href={`sftp://${username}.${id}@${formatIp(sftp.ip)}:${sftp.port}`}
|
||||
>
|
||||
Launch SFTP
|
||||
</LinkButton>
|
||||
|
@ -63,3 +63,7 @@ export function encodePathSegments (path: string): string {
|
||||
export function hashToPath (hash: string): string {
|
||||
return hash.length > 0 ? decodeURIComponent(hash.substr(1)) : '/';
|
||||
}
|
||||
|
||||
export function formatIp (ip: string): string {
|
||||
return /([a-f0-9:]+:+)+[a-f0-9]+/.test(ip) ? `[${ip}]` : ip;
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ export type SettableModalProps = Omit<ModalProps, 'appear' | 'visible' | 'onDism
|
||||
interface State {
|
||||
render: boolean;
|
||||
visible: boolean;
|
||||
showSpinnerOverlay?: boolean;
|
||||
propOverrides: Partial<SettableModalProps>;
|
||||
}
|
||||
|
||||
@ -31,7 +30,6 @@ function asModal<P extends {}> (modalProps?: SettableModalProps | ((props: P) =>
|
||||
this.state = {
|
||||
render: props.visible,
|
||||
visible: props.visible,
|
||||
showSpinnerOverlay: undefined,
|
||||
propOverrides: {},
|
||||
};
|
||||
}
|
||||
@ -39,7 +37,6 @@ function asModal<P extends {}> (modalProps?: SettableModalProps | ((props: P) =>
|
||||
get computedModalProps (): Readonly<SettableModalProps & { visible: boolean }> {
|
||||
return {
|
||||
...(typeof modalProps === 'function' ? modalProps(this.props) : modalProps),
|
||||
showSpinnerOverlay: this.state.showSpinnerOverlay,
|
||||
...this.state.propOverrides,
|
||||
visible: this.state.visible,
|
||||
};
|
||||
@ -50,7 +47,7 @@ function asModal<P extends {}> (modalProps?: SettableModalProps | ((props: P) =>
|
||||
*/
|
||||
componentDidUpdate (prevProps: Readonly<P & AsModalProps>, prevState: Readonly<State>) {
|
||||
if (prevProps.visible && !this.props.visible) {
|
||||
this.setState({ visible: false, showSpinnerOverlay: false });
|
||||
this.setState({ visible: false, propOverrides: {} });
|
||||
} else if (!prevProps.visible && this.props.visible) {
|
||||
this.setState({ render: true, visible: true });
|
||||
}
|
||||
|
@ -10,13 +10,7 @@ export interface Mode {
|
||||
|
||||
const modes: Mode[] = [
|
||||
{ name: 'C', mime: 'text/x-csrc', mode: 'clike', ext: [ 'c', 'h', 'ino' ] },
|
||||
{
|
||||
name: 'C++',
|
||||
mime: 'text/x-c++src',
|
||||
mode: 'clike',
|
||||
ext: [ 'cpp', 'c++', 'cc', 'cxx', 'hpp', 'h++', 'hh', 'hxx' ],
|
||||
alias: [ 'cpp' ],
|
||||
},
|
||||
{ name: 'C++', mime: 'text/x-c++src', mode: 'clike', ext: [ 'cpp', 'c++', 'cc', 'cxx', 'hpp', 'h++', 'hh', 'hxx' ], alias: [ 'cpp' ] },
|
||||
{ name: 'C#', mime: 'text/x-csharp', mode: 'clike', ext: [ 'cs' ], alias: [ 'csharp', 'cs' ] },
|
||||
{ name: 'CSS', mime: 'text/css', mode: 'css', ext: [ 'css' ] },
|
||||
{ name: 'CQL', mime: 'text/x-cassandra', mode: 'sql', ext: [ 'cql' ] },
|
||||
@ -24,99 +18,34 @@ const modes: Mode[] = [
|
||||
{ name: 'Dockerfile', mime: 'text/x-dockerfile', mode: 'dockerfile', file: /^Dockerfile$/ },
|
||||
{ name: 'Git Markdown', mime: 'text/x-gfm', mode: 'gfm', file: /^(readme|contributing|history|license).md$/i },
|
||||
{ name: 'Golang', mime: 'text/x-go', mode: 'go', ext: [ 'go' ] },
|
||||
{
|
||||
name: 'HTML',
|
||||
mime: 'text/html',
|
||||
mode: 'htmlmixed',
|
||||
ext: [ 'html', 'htm', 'handlebars', 'hbs' ],
|
||||
alias: [ 'xhtml' ],
|
||||
},
|
||||
{ name: 'HTML', mime: 'text/html', mode: 'htmlmixed', ext: [ 'html', 'htm', 'handlebars', 'hbs' ], alias: [ 'xhtml' ] },
|
||||
{ name: 'HTTP', mime: 'message/http', mode: 'http' },
|
||||
{
|
||||
name: 'JavaScript',
|
||||
mime: 'text/javascript',
|
||||
mimes: [ 'text/javascript', 'text/ecmascript', 'application/javascript', 'application/x-javascript', 'application/ecmascript' ],
|
||||
mode: 'javascript',
|
||||
ext: [ 'js' ],
|
||||
alias: [ 'ecmascript', 'js', 'node' ],
|
||||
},
|
||||
{
|
||||
name: 'JSON',
|
||||
mime: 'application/json',
|
||||
mimes: [ 'application/json', 'application/x-json' ],
|
||||
mode: 'javascript',
|
||||
ext: [ 'json', 'map' ],
|
||||
alias: [ 'json5' ],
|
||||
},
|
||||
{ name: 'JavaScript', mime: 'text/javascript', mimes: [ 'text/javascript', 'text/ecmascript', 'application/javascript', 'application/x-javascript', 'application/ecmascript' ], mode: 'javascript', ext: [ 'js' ], alias: [ 'ecmascript', 'js', 'node' ] },
|
||||
{ name: 'JSON', mime: 'application/json', mimes: [ 'application/json', 'application/x-json' ], mode: 'javascript', ext: [ 'json', 'map' ], alias: [ 'json5' ] },
|
||||
{ name: 'Lua', mime: 'text/x-lua', mode: 'lua', ext: [ 'lua' ] },
|
||||
{ name: 'Markdown', mime: 'text/x-markdown', mode: 'markdown', ext: [ 'markdown', 'md', 'mkd' ] },
|
||||
{ name: 'MariaDB', mime: 'text/x-mariadb', mode: 'sql' },
|
||||
{ name: 'MS SQL', mime: 'text/x-mssql', mode: 'sql' },
|
||||
{ name: 'MySQL', mime: 'text/x-mysql', mode: 'sql' },
|
||||
{ name: 'Nginx', mime: 'text/x-nginx-conf', mode: 'nginx', file: /nginx.*\.conf$/i },
|
||||
{
|
||||
name: 'PHP',
|
||||
mime: 'text/x-php',
|
||||
mimes: [ 'text/x-php', 'application/x-httpd-php', 'application/x-httpd-php-open' ],
|
||||
mode: 'php',
|
||||
ext: [ 'php', 'php3', 'php4', 'php5', 'php7', 'phtml' ],
|
||||
},
|
||||
{ name: 'PHP', mime: 'text/x-php', mimes: [ 'text/x-php', 'application/x-httpd-php', 'application/x-httpd-php-open' ], mode: 'php', ext: [ 'php', 'php3', 'php4', 'php5', 'php7', 'phtml' ] },
|
||||
{ name: 'Plain Text', mime: 'text/plain', mode: 'null', ext: [ 'txt', 'text', 'conf', 'def', 'list', 'log' ] },
|
||||
{ name: 'PostgreSQL', mime: 'text/x-pgsql', mode: 'sql' },
|
||||
{
|
||||
name: 'Properties',
|
||||
mime: 'text/x-properties',
|
||||
mode: 'properties',
|
||||
ext: [ 'properties', 'ini', 'in' ],
|
||||
alias: [ 'ini', 'properties' ],
|
||||
},
|
||||
{
|
||||
name: 'Python',
|
||||
mime: 'text/x-python',
|
||||
mode: 'python',
|
||||
ext: [ 'BUILD', 'bzl', 'py', 'pyw' ],
|
||||
file: /^(BUCK|BUILD)$/,
|
||||
},
|
||||
{
|
||||
name: 'Ruby',
|
||||
mime: 'text/x-ruby',
|
||||
mode: 'ruby',
|
||||
ext: [ 'rb' ],
|
||||
alias: [ 'jruby', 'macruby', 'rake', 'rb', 'rbx' ],
|
||||
},
|
||||
{ name: 'Properties', mime: 'text/x-properties', mode: 'properties', ext: [ 'properties', 'ini', 'in' ], alias: [ 'ini', 'properties' ] },
|
||||
{ name: 'Pug', mime: 'text/x-pug', mimes: [ 'text/x-pug', 'text/x-jade' ], mode: 'null', ext: [ 'pug' ] },
|
||||
{ name: 'Python', mime: 'text/x-python', mode: 'python', ext: [ 'BUILD', 'bzl', 'py', 'pyw' ], file: /^(BUCK|BUILD)$/ },
|
||||
{ name: 'Ruby', mime: 'text/x-ruby', mode: 'ruby', ext: [ 'rb' ], alias: [ 'jruby', 'macruby', 'rake', 'rb', 'rbx' ] },
|
||||
{ name: 'Rust', mime: 'text/x-rustsrc', mode: 'rust', ext: [ 'rs' ] },
|
||||
{ name: 'Sass', mime: 'text/x-sass', mode: 'sass', ext: [ 'sass' ] },
|
||||
{ name: 'SCSS', mime: 'text/x-scss', mode: 'css', ext: [ 'scss' ] },
|
||||
{
|
||||
name: 'Shell',
|
||||
mime: 'text/x-sh',
|
||||
mimes: [ 'text/x-sh', 'application/x-sh' ],
|
||||
mode: 'shell',
|
||||
ext: [ 'sh', 'ksh', 'bash' ],
|
||||
alias: [ 'bash', 'sh', 'zsh' ],
|
||||
file: /^PKGBUILD$/,
|
||||
},
|
||||
{ name: 'Shell', mime: 'text/x-sh', mimes: [ 'text/x-sh', 'application/x-sh' ], mode: 'shell', ext: [ 'sh', 'ksh', 'bash' ], alias: [ 'bash', 'sh', 'zsh' ], file: /^PKGBUILD$/ },
|
||||
{ name: 'SQL', mime: 'text/x-sql', mode: 'sql', ext: [ 'sql' ] },
|
||||
{ name: 'SQLite', mime: 'text/x-sqlite', mode: 'sql' },
|
||||
{ name: 'TOML', mime: 'text/x-toml', mode: 'toml', ext: [ 'toml' ] },
|
||||
{ name: 'TypeScript', mime: 'application/typescript', mode: 'javascript', ext: [ 'ts' ], alias: [ 'ts' ] },
|
||||
{ name: 'Vue', mime: 'script/x-vue', mimes: [ 'script/x-vue', 'text/x-vue' ], mode: 'vue', ext: [ 'vue' ] },
|
||||
{
|
||||
name: 'XML',
|
||||
mime: 'application/xml',
|
||||
mimes: [ 'application/xml', 'text/xml' ],
|
||||
mode: 'xml',
|
||||
ext: [ 'xml', 'xsl', 'xsd', 'svg' ],
|
||||
alias: [ 'rss', 'wsdl', 'xsd' ],
|
||||
},
|
||||
{
|
||||
name: 'YAML',
|
||||
mime: 'text/x-yaml',
|
||||
mimes: [ 'text/x-yaml', 'text/yaml' ],
|
||||
mode: 'yaml',
|
||||
ext: [ 'yaml', 'yml' ],
|
||||
alias: [ 'yml' ],
|
||||
},
|
||||
{ name: 'XML', mime: 'application/xml', mimes: [ 'application/xml', 'text/xml' ], mode: 'xml', ext: [ 'xml', 'xsl', 'xsd', 'svg' ], alias: [ 'rss', 'wsdl', 'xsd' ] },
|
||||
{ name: 'YAML', mime: 'text/x-yaml', mimes: [ 'text/x-yaml', 'text/yaml' ], mode: 'yaml', ext: [ 'yaml', 'yml' ], alias: [ 'yml' ] },
|
||||
];
|
||||
|
||||
export default modes;
|
||||
|
@ -3,12 +3,14 @@ import loadDirectory, { FileObject } from '@/api/server/files/loadDirectory';
|
||||
import { cleanDirectoryPath } from '@/helpers';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
export const getDirectorySwrKey = (uuid: string, directory: string): string => `${uuid}:files:${directory}`;
|
||||
|
||||
export default () => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
|
||||
return useSWR<FileObject[]>(
|
||||
`${uuid}:files:${directory}`,
|
||||
getDirectorySwrKey(uuid, directory),
|
||||
() => loadDirectory(uuid, cleanDirectoryPath(directory)),
|
||||
{
|
||||
focusThrottleInterval: 30000,
|
||||
|
@ -70,7 +70,11 @@
|
||||
@parent
|
||||
<script>
|
||||
$('#configTokenBtn').on('click', function (event) {
|
||||
$.getJSON('{{ route('admin.nodes.view.configuration.token', $node->id) }}').done(function (data) {
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '{{ route('admin.nodes.view.configuration.token', $node->id) }}',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||
}).done(function (data) {
|
||||
swal({
|
||||
type: 'success',
|
||||
title: 'Token created.',
|
||||
|
@ -145,9 +145,9 @@
|
||||
showLoaderOnConfirm: true
|
||||
}, function () {
|
||||
$.ajax({
|
||||
method: 'GET',
|
||||
method: 'POST',
|
||||
url: '/admin/settings/mail/test',
|
||||
headers: { 'X-CSRF-Token': $('input[name="_token"]').val() }
|
||||
headers: { 'X-CSRF-TOKEN': $('input[name="_token"]').val() }
|
||||
}).fail(function (jqXHR) {
|
||||
showErrorDialog(jqXHR, 'test');
|
||||
}).done(function () {
|
||||
|
@ -195,9 +195,17 @@
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Log out'
|
||||
}, function () {
|
||||
window.location = $(that).attr('href');
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '{{ route('auth.logout') }}',
|
||||
data: {
|
||||
_token: '{{ csrf_token() }}'
|
||||
},complete: function () {
|
||||
window.location.href = '{{route('auth.login')}}';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
|
||||
|
@ -66,8 +66,8 @@ Route::group(['prefix' => 'databases'], function () {
|
||||
Route::group(['prefix' => 'settings'], function () {
|
||||
Route::get('/', 'Settings\IndexController@index')->name('admin.settings');
|
||||
Route::get('/mail', 'Settings\MailController@index')->name('admin.settings.mail');
|
||||
Route::get('/mail/test', 'Settings\MailController@test')->name('admin.settings.mail.test');
|
||||
Route::get('/advanced', 'Settings\AdvancedController@index')->name('admin.settings.advanced');
|
||||
Route::post('/mail/test', 'Settings\MailController@test')->name('admin.settings.mail.test');
|
||||
|
||||
Route::patch('/', 'Settings\IndexController@update');
|
||||
Route::patch('/mail', 'Settings\MailController@update');
|
||||
@ -153,12 +153,12 @@ Route::group(['prefix' => 'nodes'], function () {
|
||||
Route::get('/view/{node}/allocation', 'Nodes\NodeViewController@allocations')->name('admin.nodes.view.allocation');
|
||||
Route::get('/view/{node}/servers', 'Nodes\NodeViewController@servers')->name('admin.nodes.view.servers');
|
||||
Route::get('/view/{node}/system-information', 'Nodes\SystemInformationController');
|
||||
Route::get('/view/{node}/settings/token', 'NodeAutoDeployController')->name('admin.nodes.view.configuration.token');
|
||||
|
||||
Route::post('/new', 'NodesController@store');
|
||||
Route::post('/view/{node}/allocation', 'NodesController@createAllocation');
|
||||
Route::post('/view/{node}/allocation/remove', 'NodesController@allocationRemoveBlock')->name('admin.nodes.view.allocation.removeBlock');
|
||||
Route::post('/view/{node}/allocation/alias', 'NodesController@allocationSetAlias')->name('admin.nodes.view.allocation.setAlias');
|
||||
Route::post('/view/{node}/settings/token', 'NodeAutoDeployController')->name('admin.nodes.view.configuration.token');
|
||||
|
||||
Route::patch('/view/{node}/settings', 'NodesController@updateSettings');
|
||||
|
||||
|
@ -15,13 +15,21 @@ Route::group(['middleware' => 'guest'], function () {
|
||||
Route::get('/password', 'LoginController@index')->name('auth.forgot-password');
|
||||
Route::get('/password/reset/{token}', 'LoginController@index')->name('auth.reset');
|
||||
|
||||
// Login endpoints.
|
||||
Route::post('/login', 'LoginController@login')->middleware('recaptcha');
|
||||
Route::post('/login/checkpoint', 'LoginCheckpointController')->name('auth.login-checkpoint');
|
||||
// Apply a throttle to authentication action endpoints, in addition to the
|
||||
// recaptcha endpoints to slow down manual attack spammers even more. 🤷
|
||||
//
|
||||
// @see \Pterodactyl\Providers\RouteServiceProvider
|
||||
Route::middleware(['throttle:authentication'])->group(function () {
|
||||
// Login endpoints.
|
||||
Route::post('/login', 'LoginController@login')->middleware('recaptcha');
|
||||
Route::post('/login/checkpoint', 'LoginCheckpointController')->name('auth.login-checkpoint');
|
||||
|
||||
// Forgot password route. A post to this endpoint will trigger an
|
||||
// email to be sent containing a reset token.
|
||||
Route::post('/password', 'ForgotPasswordController@sendResetLinkEmail')->middleware('recaptcha');
|
||||
// Forgot password route. A post to this endpoint will trigger an
|
||||
// email to be sent containing a reset token.
|
||||
Route::post('/password', 'ForgotPasswordController@sendResetLinkEmail')
|
||||
->name('auth.post.forgot-password')
|
||||
->middleware('recaptcha');
|
||||
});
|
||||
|
||||
// Password reset routes. This endpoint is hit after going through
|
||||
// the forgot password routes to acquire a token (or after an account
|
||||
@ -40,4 +48,4 @@ Route::group(['middleware' => 'guest'], function () {
|
||||
| Endpoint: /auth
|
||||
|
|
||||
*/
|
||||
Route::get('/logout', 'LoginController@logout')->name('auth.logout')->middleware('auth');
|
||||
Route::post('/logout', 'LoginController@logout')->name('auth.logout')->middleware('auth', 'csrf');
|
||||
|
@ -5,6 +5,7 @@ namespace Pterodactyl\Tests\Integration\Api\Application\Location;
|
||||
use Pterodactyl\Models\Node;
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Location;
|
||||
use Pterodactyl\Transformers\Api\Application\LocationTransformer;
|
||||
use Pterodactyl\Transformers\Api\Application\NodeTransformer;
|
||||
use Pterodactyl\Transformers\Api\Application\ServerTransformer;
|
||||
use Pterodactyl\Tests\Integration\Api\Application\ApplicationApiIntegrationTestCase;
|
||||
@ -88,6 +89,77 @@ class LocationControllerTest extends ApplicationApiIntegrationTestCase
|
||||
], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a location can be created.
|
||||
*/
|
||||
public function testCreateLocation()
|
||||
{
|
||||
$response = $this->postJson('/api/application/locations', [
|
||||
'short' => 'inhouse',
|
||||
'long' => 'This is my inhouse location',
|
||||
]);
|
||||
|
||||
$response->assertStatus(Response::HTTP_CREATED);
|
||||
$response->assertJsonCount(3);
|
||||
$response->assertJsonStructure([
|
||||
'object',
|
||||
'attributes' => ['id', 'short', 'long', 'created_at', 'updated_at'],
|
||||
'meta' => ['resource'],
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('locations', ['short' => 'inhouse', 'long' => 'This is my inhouse location']);
|
||||
|
||||
$location = Location::where('short', 'inhouse')->first();
|
||||
$response->assertJson([
|
||||
'object' => 'location',
|
||||
'attributes' => $this->getTransformer(LocationTransformer::class)->transform($location),
|
||||
'meta' => [
|
||||
'resource' => route('api.application.locations.view', $location->id),
|
||||
],
|
||||
], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a location can be updated.
|
||||
*/
|
||||
public function testUpdateLocation()
|
||||
{
|
||||
$location = Location::factory()->create();
|
||||
|
||||
$response = $this->patchJson('/api/application/locations/' . $location->id, [
|
||||
'short' => 'new inhouse',
|
||||
'long' => 'This is my new inhouse location'
|
||||
]);
|
||||
$response->assertStatus(Response::HTTP_OK);
|
||||
$response->assertJsonCount(2);
|
||||
$response->assertJsonStructure([
|
||||
'object',
|
||||
'attributes' => ['id', 'short', 'long', 'created_at', 'updated_at']
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('locations', ['short' => 'new inhouse', 'long' => 'This is my new inhouse location']);
|
||||
$location = $location->fresh();
|
||||
|
||||
$response->assertJson([
|
||||
'object' => 'location',
|
||||
'attributes' => $this->getTransformer(LocationTransformer::class)->transform($location),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a location can be deleted from the database.
|
||||
*/
|
||||
public function testDeleteLocation()
|
||||
{
|
||||
$location = Location::factory()->create();
|
||||
$this->assertDatabaseHas('locations', ['id' => $location->id]);
|
||||
|
||||
$response = $this->delete('/api/application/locations/' . $location->id);
|
||||
$response->assertStatus(Response::HTTP_NO_CONTENT);
|
||||
|
||||
$this->assertDatabaseMissing('locations', ['id' => $location->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that all of the defined relationships for a location can be loaded successfully.
|
||||
*/
|
||||
|
@ -26,7 +26,7 @@ class EggControllerTest extends ApplicationApiIntegrationTestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that all of the eggs belonging to a given nest can be returned.
|
||||
* Test that all the eggs belonging to a given nest can be returned.
|
||||
*/
|
||||
public function testListAllEggsInNest()
|
||||
{
|
||||
@ -47,7 +47,7 @@ class EggControllerTest extends ApplicationApiIntegrationTestCase
|
||||
'files' => [],
|
||||
'startup' => ['done'],
|
||||
'stop',
|
||||
'logs' => ['custom', 'location'],
|
||||
'logs' => [],
|
||||
'extends',
|
||||
],
|
||||
],
|
||||
|
@ -26,8 +26,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->daemonServerRepository = Mockery::mock(DaemonServerRepository::class);
|
||||
$this->swap(DaemonServerRepository::class, $this->daemonServerRepository);
|
||||
$this->daemonServerRepository = $this->mock(DaemonServerRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,7 +49,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
||||
$allocations[2]->update(['server_id' => $server2->id]);
|
||||
$allocations[3]->update(['server_id' => $server2->id]);
|
||||
|
||||
$this->daemonServerRepository->expects('setServer->update')->andReturnUndefined();
|
||||
$this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined();
|
||||
|
||||
$response = $this->getService()->handle($server, [
|
||||
// Attempt to add one new allocation, and an allocation assigned to another server. The
|
||||
@ -113,20 +112,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
||||
return $s->id === $server->id;
|
||||
}))->andReturnSelf();
|
||||
|
||||
$this->daemonServerRepository->expects('update')->with(Mockery::on(function ($data) {
|
||||
$this->assertEquals([
|
||||
'build' => [
|
||||
'memory_limit' => 256,
|
||||
'swap' => 128,
|
||||
'io_weight' => 600,
|
||||
'cpu_limit' => 150,
|
||||
'threads' => '1,2',
|
||||
'disk_space' => 1024,
|
||||
],
|
||||
], $data);
|
||||
|
||||
return true;
|
||||
}))->andReturnUndefined();
|
||||
$this->daemonServerRepository->expects('sync')->withNoArgs()->andReturnUndefined();
|
||||
|
||||
$response = $this->getService()->handle($server, [
|
||||
'oom_disabled' => false,
|
||||
@ -162,7 +148,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
||||
{
|
||||
$server = $this->createServerModel();
|
||||
|
||||
$this->daemonServerRepository->expects('setServer->update')->andThrows(
|
||||
$this->daemonServerRepository->expects('setServer->sync')->andThrows(
|
||||
new DaemonConnectionException(
|
||||
new RequestException('Bad request', new Request('GET', '/test'), new Response())
|
||||
)
|
||||
@ -186,7 +172,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
||||
/** @var \Pterodactyl\Models\Allocation $allocation */
|
||||
$allocation = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server->id]);
|
||||
|
||||
$this->daemonServerRepository->expects('setServer->update')->andReturnUndefined();
|
||||
$this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined();
|
||||
|
||||
$this->getService()->handle($server, [
|
||||
'remove_allocations' => [$allocation->id],
|
||||
@ -209,7 +195,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
||||
/** @var \Pterodactyl\Models\Allocation $allocation */
|
||||
$allocation = Allocation::factory()->create(['node_id' => $server->node_id]);
|
||||
|
||||
$this->daemonServerRepository->expects('setServer->update')->andReturnUndefined();
|
||||
$this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined();
|
||||
|
||||
$this->getService()->handle($server, [
|
||||
'add_allocations' => [$allocation->id],
|
||||
@ -230,7 +216,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
||||
/** @var \Pterodactyl\Models\Allocation $allocation2 */
|
||||
$allocation2 = Allocation::factory()->create(['node_id' => $server->node_id]);
|
||||
|
||||
$this->daemonServerRepository->expects('setServer->update')->andReturnUndefined();
|
||||
$this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined();
|
||||
|
||||
$this->getService()->handle($server, [
|
||||
'add_allocations' => [$allocation2->id, $allocation2->id],
|
||||
@ -253,7 +239,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
||||
/** @var \Pterodactyl\Models\Allocation $allocation */
|
||||
$allocation = Allocation::factory()->create(['node_id' => $server->node_id]);
|
||||
|
||||
$this->daemonServerRepository->expects('setServer->update')->andThrows(new DisplayException('Test'));
|
||||
$this->daemonServerRepository->expects('setServer->sync')->andThrows(new DisplayException('Test'));
|
||||
|
||||
$this->expectException(DisplayException::class);
|
||||
|
||||
|
@ -67,7 +67,6 @@ class ServerCreationServiceTest extends IntegrationTestCase
|
||||
$allocations[0]->port,
|
||||
]);
|
||||
|
||||
/** @noinspection PhpParamsInspection */
|
||||
$egg = $this->cloneEggAndVariables(Egg::query()->findOrFail(1));
|
||||
// We want to make sure that the validator service runs as an admin, and not as a regular
|
||||
// user when saving variables.
|
||||
@ -94,19 +93,10 @@ class ServerCreationServiceTest extends IntegrationTestCase
|
||||
'BUNGEE_VERSION' => '123',
|
||||
'SERVER_JARFILE' => 'server2.jar',
|
||||
],
|
||||
'start_on_completion' => true,
|
||||
];
|
||||
|
||||
$this->daemonServerRepository->expects('setServer')->andReturnSelf();
|
||||
$this->daemonServerRepository->expects('create')->with(Mockery::on(function ($value) {
|
||||
$this->assertIsArray($value);
|
||||
// Just check for some keys to make sure we're getting the expected configuration
|
||||
// structure back. Other tests exist to confirm it is the correct structure.
|
||||
$this->assertArrayHasKey('uuid', $value);
|
||||
$this->assertArrayHasKey('environment', $value);
|
||||
$this->assertArrayHasKey('invocation', $value);
|
||||
|
||||
return true;
|
||||
}))->andReturnUndefined();
|
||||
$this->daemonServerRepository->expects('setServer->create')->with(true)->andReturnUndefined();
|
||||
|
||||
try {
|
||||
$this->getService()->handle(array_merge($data, [
|
||||
@ -115,7 +105,8 @@ class ServerCreationServiceTest extends IntegrationTestCase
|
||||
'SERVER_JARFILE' => 'server2.jar',
|
||||
],
|
||||
]), $deployment);
|
||||
$this->assertTrue(false, 'This statement should not be reached.');
|
||||
|
||||
$this->fail('This execution pathway should not be reached.');
|
||||
} catch (ValidationException $exception) {
|
||||
$this->assertCount(1, $exception->errors());
|
||||
$this->assertArrayHasKey('environment.BUNGEE_VERSION', $exception->errors());
|
||||
@ -133,11 +124,11 @@ class ServerCreationServiceTest extends IntegrationTestCase
|
||||
$this->assertSame('server2.jar', $response->variables[1]->server_value);
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array($key, ['allocation_additional', 'environment'])) {
|
||||
if (in_array($key, ['allocation_additional', 'environment', 'start_on_completion'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertSame($value, $response->{$key});
|
||||
$this->assertSame($value, $response->{$key}, "Failed asserting equality of '$key' in server response. Got: [{$response->{$key}}] Expected: [$value]");
|
||||
}
|
||||
|
||||
$this->assertCount(2, $response->allocations);
|
||||
|
@ -29,20 +29,15 @@ class SuspensionServiceTest extends IntegrationTestCase
|
||||
{
|
||||
$server = $this->createServerModel();
|
||||
|
||||
$this->repository->expects('setServer')->twice()->andReturnSelf();
|
||||
$this->repository->expects('suspend')->with(false)->andReturnUndefined();
|
||||
$this->repository->expects('setServer->sync')->twice()->andReturnSelf();
|
||||
|
||||
$this->getService()->toggle($server, SuspensionService::ACTION_SUSPEND);
|
||||
|
||||
$server->refresh();
|
||||
$this->assertTrue($server->isSuspended());
|
||||
|
||||
$this->repository->expects('suspend')->with(true)->andReturnUndefined();
|
||||
$this->assertTrue($server->refresh()->isSuspended());
|
||||
|
||||
$this->getService()->toggle($server, SuspensionService::ACTION_UNSUSPEND);
|
||||
|
||||
$server->refresh();
|
||||
$this->assertFalse($server->isSuspended());
|
||||
$this->assertFalse($server->refresh()->isSuspended());
|
||||
}
|
||||
|
||||
public function testNoActionIsTakenIfSuspensionStatusIsUnchanged()
|
||||
|
@ -1,168 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Tests\Unit\Http\Middleware\Api;
|
||||
|
||||
use Mockery as m;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\ApiKey;
|
||||
use Illuminate\Auth\AuthManager;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Pterodactyl\Http\Middleware\Api\AuthenticateKey;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||
use Pterodactyl\Tests\Unit\Http\Middleware\MiddlewareTestCase;
|
||||
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
class AuthenticateKeyTest extends MiddlewareTestCase
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Auth\AuthManager|\Mockery\Mock
|
||||
*/
|
||||
private $auth;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock
|
||||
*/
|
||||
private $encrypter;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface|\Mockery\Mock
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* Setup tests.
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->auth = m::mock(AuthManager::class);
|
||||
$this->encrypter = m::mock(Encrypter::class);
|
||||
$this->repository = m::mock(ApiKeyRepositoryInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a missing bearer token will throw an exception.
|
||||
*/
|
||||
public function testMissingBearerTokenThrowsException()
|
||||
{
|
||||
$this->request->shouldReceive('user')->andReturnNull();
|
||||
$this->request->shouldReceive('bearerToken')->withNoArgs()->once()->andReturnNull();
|
||||
|
||||
try {
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION);
|
||||
} catch (HttpException $exception) {
|
||||
$this->assertEquals(401, $exception->getStatusCode());
|
||||
$this->assertEquals(['WWW-Authenticate' => 'Bearer'], $exception->getHeaders());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an invalid API identifier throws an exception.
|
||||
*/
|
||||
public function testInvalidIdentifier()
|
||||
{
|
||||
$this->expectException(AccessDeniedHttpException::class);
|
||||
|
||||
$this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn('abcd1234');
|
||||
$this->repository->shouldReceive('findFirstWhere')->andThrow(new RecordNotFoundException());
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a valid token can continue past the middleware.
|
||||
*/
|
||||
public function testValidToken()
|
||||
{
|
||||
$model = ApiKey::factory()->make();
|
||||
|
||||
$this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->identifier . 'decrypted');
|
||||
$this->repository->shouldReceive('findFirstWhere')->with([
|
||||
['identifier', '=', $model->identifier],
|
||||
['key_type', '=', ApiKey::TYPE_APPLICATION],
|
||||
])->once()->andReturn($model);
|
||||
$this->encrypter->shouldReceive('decrypt')->with($model->token)->once()->andReturn('decrypted');
|
||||
$this->auth->shouldReceive('guard->loginUsingId')->with($model->user_id)->once()->andReturnNull();
|
||||
|
||||
$this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, [
|
||||
'last_used_at' => CarbonImmutable::now(),
|
||||
])->once()->andReturnNull();
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION);
|
||||
$this->assertEquals($model, $this->request->attributes->get('api_key'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a valid token can continue past the middleware when set as a user token.
|
||||
*/
|
||||
public function testValidTokenWithUserKey()
|
||||
{
|
||||
$model = ApiKey::factory()->make();
|
||||
|
||||
$this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->identifier . 'decrypted');
|
||||
$this->repository->shouldReceive('findFirstWhere')->with([
|
||||
['identifier', '=', $model->identifier],
|
||||
['key_type', '=', ApiKey::TYPE_ACCOUNT],
|
||||
])->once()->andReturn($model);
|
||||
$this->encrypter->shouldReceive('decrypt')->with($model->token)->once()->andReturn('decrypted');
|
||||
$this->auth->shouldReceive('guard->loginUsingId')->with($model->user_id)->once()->andReturnNull();
|
||||
|
||||
$this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, [
|
||||
'last_used_at' => CarbonImmutable::now(),
|
||||
])->once()->andReturnNull();
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_ACCOUNT);
|
||||
$this->assertEquals($model, $this->request->attributes->get('api_key'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that we can still make it though this middleware if the user is logged in and passing
|
||||
* through a cookie.
|
||||
*/
|
||||
public function testAccessWithoutToken()
|
||||
{
|
||||
$user = User::factory()->make(['id' => 123]);
|
||||
|
||||
$this->request->shouldReceive('user')->andReturn($user);
|
||||
$this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturnNull();
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_ACCOUNT);
|
||||
$model = $this->request->attributes->get('api_key');
|
||||
|
||||
$this->assertSame(ApiKey::TYPE_ACCOUNT, $model->key_type);
|
||||
$this->assertSame(123, $model->user_id);
|
||||
$this->assertNull($model->identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a valid token identifier with an invalid token attached to it
|
||||
* triggers an exception.
|
||||
*/
|
||||
public function testInvalidTokenForIdentifier()
|
||||
{
|
||||
$this->expectException(AccessDeniedHttpException::class);
|
||||
|
||||
$model = ApiKey::factory()->make();
|
||||
|
||||
$this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->identifier . 'asdf');
|
||||
$this->repository->shouldReceive('findFirstWhere')->with([
|
||||
['identifier', '=', $model->identifier],
|
||||
['key_type', '=', ApiKey::TYPE_APPLICATION],
|
||||
])->once()->andReturn($model);
|
||||
$this->encrypter->shouldReceive('decrypt')->with($model->token)->once()->andReturn('decrypted');
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of the middleware with mocked dependencies for testing.
|
||||
*/
|
||||
private function getMiddleware(): AuthenticateKey
|
||||
{
|
||||
return new AuthenticateKey($this->repository, $this->auth, $this->encrypter);
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Tests\Unit\Http\Middleware\Api;
|
||||
|
||||
use Mockery as m;
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Pterodactyl\Http\Middleware\Api\SetSessionDriver;
|
||||
use Pterodactyl\Tests\Unit\Http\Middleware\MiddlewareTestCase;
|
||||
|
||||
class SetSessionDriverTest extends MiddlewareTestCase
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* Setup tests.
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->config = m::mock(Repository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a production environment does not try to disable debug bar.
|
||||
*/
|
||||
public function testMiddleware()
|
||||
{
|
||||
$this->config->shouldReceive('set')->once()->with('session.driver', 'array')->andReturnNull();
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of the middleware with mocked dependencies for testing.
|
||||
*/
|
||||
private function getMiddleware(): SetSessionDriver
|
||||
{
|
||||
return new SetSessionDriver($this->config);
|
||||
}
|
||||
}
|
@ -8040,10 +8040,10 @@ xterm-addon-web-links@^0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0.tgz#265cbf8221b9b315d0a748e1323bee331cd5da03"
|
||||
integrity sha512-xv8GeiINmx0zENO9hf5k+5bnkaE8mRzF+OBAr9WeFq2eLaQSudioQSiT34M1ofKbzcdjSsKiZm19Rw3i4eXamg==
|
||||
|
||||
xterm@^4.12.0:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.12.0.tgz#db09b425b4dcae5b96f8cbbaaa93b3bc60997ca9"
|
||||
integrity sha512-K5mF/p3txUV18mjiZFlElagoHFpqXrm5OYHeoymeXSu8GG/nMaOO/+NRcNCwfdjzAbdQ5VLF32hEHiWWKKm0bw==
|
||||
xterm@^4.15.0:
|
||||
version "4.15.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.15.0.tgz#e52038507eba7e0d36d47f81e29fe548c82b9561"
|
||||
integrity sha512-Ik1GoSq1yqKZQ2LF37RPS01kX9t4TP8gpamUYblD09yvWX5mEYuMK4CcqH6+plgiNEZduhTz/UrcaWs97gOlOw==
|
||||
|
||||
y18n@^4.0.0:
|
||||
version "4.0.0"
|
||||
|
Loading…
Reference in New Issue
Block a user