Compare commits

..

69 Commits

Author SHA1 Message Date
Boy132
1d02365efe
Fix eslint warnings (#3814) 2021-12-19 09:50:18 -08:00
wingdings255
1564742606
Update egg-ark--survival-evolved.json (#3809) 2021-12-19 09:44:32 -08:00
Mrxbox98
66c56b0da8
CPU Graph change (#3804) 2021-12-12 10:19:06 -08:00
Mrxbox98
15619fb8e4
Fixes overlapping Two Factor Authentication Box (#3803) 2021-12-12 10:15:44 -08:00
Alex
4e6fe112b0
fix(forge): actually fix forge regex (#3801)
For whatever reason, stupid me rebased the wrong branch in previous PR #3783 and didn't notice it, which contained the old egg instead.

This one actually fixes the regex and includes more debugging steps for easier troubleshooting.

Easy to view diff: <https://www.diffchecker.com/3iJ9lVzH>
2021-12-12 10:14:25 -08:00
Mrxbox98
f04b87a37c
FireFox Font Fix (#3805) 2021-12-12 10:14:09 -08:00
hz-ad
928b060647
Include HostEZ in sponsors list (#3788)
Update to append HostEZ to the sponsors list
2021-12-06 11:00:29 -08:00
Alex
b8bf537737
cmd(setup): validate email input, closes #3175 (#3716) 2021-12-04 10:52:09 -08:00
Charles Morgan
0d5ff6afac
Add Pug / Jade to file editor, closes #3512 (#3514) 2021-12-04 10:51:52 -08:00
Alex
5cde059f21
docs(docker): display correct variable for certificates (#3723)
Docker image and compose file uses `LE_EMAIL` and not `LETSENCRYPT_EMAIL`

Co-authored-by: Matthew Penner <me@matthewp.io>
2021-12-04 10:51:15 -08:00
Alex
0db772a82b
eggs: update source install script (#3604)
Installing basic packages are unnecessary as they already exist in yolks installer image. This also gets rid of Debian 10 lib32gcc package not being found, since installer image is Debian 11.
2021-12-04 10:50:50 -08:00
Paul Vogel
dcbc1360a9
Improve test coverage for LocationController (#3779)
By adding tests for create, update, delete
2021-12-04 10:50:36 -08:00
Patrick R
622b939f00
Show ipv6 with correct in-url syntax (#3776) 2021-12-04 10:35:55 -08:00
Lukas Moucka
e8e2911a92
Change order of docker images in JavaVersionModalFeature (#3782)
This changes the order of the Docker images in JavaVersionModalFeature, and also sets the default state to Java 17. Previously it was Java 16, even though the first entry in the list was Java 8, that confused a lot of people
2021-12-04 10:35:39 -08:00
Boy132
96c3338e96
Add the MC 1.18 message to Java Version Modal (#3778) 2021-12-04 10:35:20 -08:00
Alex
49d5ef271d
ARM64 support for the Panel Docker image, closes #3580 (#3709)
Co-authored-by: Dane Everitt <dane@daneeveritt.com>
2021-12-04 10:33:42 -08:00
Boy132
4cc8658334
GSL Token Modal Feature (#3746) 2021-12-04 10:29:24 -08:00
Yusta
a6e0e5dbda
Add app_url for mail sender (#3753)
Co-authored-by: Dane Everitt <dane@daneeveritt.com>
2021-12-04 10:26:00 -08:00
Desjardins Jérôme
10aaf00e83
use DB_PORT for mysql database connection (#3762)
DB_PORT is the env variable defined for the mysql port into Panel Configuration.
2021-12-04 10:25:02 -08:00
Paul Vogel
b9d73afb63
Fix typo in messsage when deleting a database (#3777) 2021-12-04 10:24:06 -08:00
Alex
59d47e746b
fix: Forge version regex for 1.17+ JPMS (#3783)
For 1.17 JPMS arguments, fix regex to match`^1\.(17|18|19|20|21|22|23)` or latest instead of only dot . minor versions, which is something I didn't notice in a previous PR. This should future proof it.

Changes Java image display order defaulting to 17, which the 1.17+ requires for unix args.
2021-12-04 10:23:37 -08:00
Alex
01e7a45cc5
fix(eggs): Forge latest version fetching (#3770)
Fixes a typo in fetching the latest versions. It was overwritten to "recommended" by mistake.

Easy to read diff: <https://www.diffchecker.com/U04gJTRu>
2021-11-29 10:14:08 -08:00
Dane Everitt
30bb629bad
Update CHANGELOG.md 2021-11-16 20:36:53 -08:00
Dane Everitt
bf9cbe2c6d
Add consistent CSRF token verification to API endpoints; address security concern with non-CSRF protected endpoints 2021-11-16 20:02:18 -08:00
Matthew Penner
cc31a0a6d0
tests(integration): don't expect non-required fields 2021-11-15 11:29:22 -07:00
Alex
01871d8a6c
add Java 17 LTS image to Minecraft eggs (#3744)
* feat: add Java 17 LTS for Minecraft

* feat: add java 17 option to java modal
2021-11-15 08:15:27 -08:00
Dane Everitt
17c03e9a4d
Fix broken session management for application api 2021-11-03 21:33:21 -07:00
Dane Everitt
e8a8405899
Remove tests 2021-11-03 21:22:14 -07:00
Dane Everitt
60eff40a0c
Fix session management on client API requests; closes #3727
Versions of Pterodactyl prior to 1.6.3 used a different throttle pathway for
requests. That pathway found the current request user before continuing on to
other in-app middleware, thus the user was available downstream.

Changes introduced in 1.6.3 changed the throttler logic, therefore removing this
step. As a result, the client API could not always get the currently authenticated
user when cookies were used (aka, requests from the Panel UI, and not API directly).

This change corrects the logic to get the session setup correctly before falling
through to authenticating as a user using the API key. If a cookie is present and a
user is found as a result that session will be used. If an API key is provided it is
ignored when a cookie is also present.

In order to keep the API stateless any session created for an API request stemming
from an API key will have the associated session deleted at the end of the request,
and the 'Set-Cookies' header will be stripped from the response.
2021-11-03 20:51:39 -07:00
Alex
d0663dcbd4
fix: use POST for admin logout route (#3710)
Quick fix for logging out from the admin panel as the auth route was changed from GET to POST.
2021-10-30 13:27:59 -07:00
Alex
4dca4f0aa9
change display format of the container uptime (#3706)
* change display format of the container uptime

Display `day, hour, min` if days is more than 0, otherwise default to existing `hour, min, sec`. Removes pads to make it more clean in this new format.

* clean the return
2021-10-24 14:41:01 -07:00
Samuel Ryberg
c4ab318d5a
Update docker-compose.example.yml (#3707) 2021-10-24 10:21:58 -07:00
Alex
ef4410bac6
expose uptime to client resources API endpoint (#3705)
resolves #3704
2021-10-24 10:12:17 -07:00
Anders G. Jørgensen
72680fc954
Don't force enable-query (#3700)
But make sure the query.port is set correctly, if query is enabled.
2021-10-23 13:11:45 -07:00
Dane Everitt
d65e2978d0
Update CHANGELOG.md 2021-10-23 13:02:25 -07:00
Dane Everitt
45999ba4ee
(security) use POST for logout rather than GET
see https://github.com/pterodactyl/panel/security/advisories/GHSA-m49f-hcxp-6hm6
2021-10-23 13:00:21 -07:00
Dane Everitt
22a8b2b3a2
Use more standardized rate limiting in Laravel; apply limits to auth routes 2021-10-23 12:17:16 -07:00
Alex
f77932a617
cmd(upgrade): Attempt to gain users attention during upgrade (#3678)
* cmd(upgrade):  Attempt to gain users attention during upgrade

Changes color of the user and group to gain attention, common issue is having wrong user/group which breaks the panel. Outputs termination message when users spam enter skipping the upgrade wondering why it didn't upgrade.

Reminder to update wings, because users forget it.

* cmd(upgrade): Display wings upgrade documentation link
2021-10-10 11:08:22 -07:00
Alex
c12f1463b0
eggs(forge): Add support for 1.17+ Forge (#3676)
Support new 1.17+ Forge JPMS arguments that don't ship any executable jar. It will use unix_args.txt file for 1.17+ when one exists, otherwise defaults to using the jar file

Fix forge latest build version option to actually use latest instead of recommended
Set build version input rules to only accept valid values of the latest and recommended
Remove spaces from the version variables to avoid issues with curl. Forge site displays versions with spaces to end users
2021-10-10 10:50:01 -07:00
Alex
5b6de4df6f
eggs(rust): custom map url (#3625)
Introduces custom map URL variable. If none is provided, it will default to using normal map size and seed. Otherwise, it will use the custom map and remove map size/seed from the startup as required.
2021-10-09 10:31:47 -07:00
Waseem Hassan Shahid
8b236c6907
Fix SSL config docker (#3616)
* Don't copy default nginx config at build time

* Use http.d folder for nginx configs

* Add default config back

* Change the panel config name
2021-10-09 10:31:29 -07:00
Matthew Penner
4fa38b8e9c
Fix wings receiving wrong suspended status on sync (#3667)
Due to wings pulling the server configuration rather than the Panel pushing it,
wings gets the wrong status for a server if both the status update and sync request
are ran in a transaction due to the status not being persisted in the database.

Fixes #3639
2021-10-07 08:46:09 -07:00
Cyra
de0d5c9b8a
Updated CHS sponsor entry to use new domain (#3659)
Updated CHS sponsor entry to use new domain
Updated from captiolsolutions.cloud to chs.gg
2021-10-04 08:23:10 -07:00
Dane Everitt
81ba333270
If uptime is present in stats output, display it for the server; closes #3653 2021-10-03 12:59:44 -07:00
Dane Everitt
63e01f9aee
Update SECURITY.md 2021-10-02 08:21:04 -07:00
Dane Everitt
c57eb2c9e6
Update CHANGELOG.md 2021-09-21 21:36:29 -07:00
Dane Everitt
4a84c36009
Fix security vulnerability when authenticating a two-factor authentication token for a user
See associated security advisory for technical details on the content of this security fix.

GHSA ID: GHSA-5vfx-8w6m-h3v4
2021-09-21 21:30:08 -07:00
Dane Everitt
5fdb0a5909
Correctly expose OOM disable state for a server 2021-09-13 21:02:12 -07:00
Dane Everitt
f5a1ce13b8
Update CHANGELOG.md 2021-09-13 20:47:30 -07:00
Matthew Penner
bc25468802
server: fix build modification not being persisted (#3610) 2021-09-12 23:18:17 -06:00
Dane Everitt
dbb061d6f3
Update CHANGELOG.md 2021-09-12 11:26:37 -07:00
Dane Everitt
8f0eda21c5
Fix all screens on the panel unintentionally loading the root directory for a server 2021-09-11 14:17:20 -07:00
Dane Everitt
52588beeb0
Fix state management of overrides not properly resetting loader; closes #3429 2021-09-11 13:24:57 -07:00
Dane Everitt
7b429831ce
Fix missing user agent headers to store an empty string rather than null value 2021-09-11 13:00:53 -07:00
Boy132
5e5d7d6689
Update egg-garrys-mod.json (#3606)
Co-authored-by: Dane Everitt <dane@daneeveritt.com>
2021-09-11 12:20:15 -07:00
Matthew Penner
4d7140bd3b
actions(tests): backport v2 workflow (#3558)
Co-authored-by: Dane Everitt <dane@daneeveritt.com>
2021-09-11 12:20:04 -07:00
Dane Everitt
0b521c011f
Fix test workflow matrix for databases 2021-09-11 12:13:15 -07:00
Dane Everitt
db74b0024d
Merge branch 'develop' of github.com:pterodactyl/panel into develop 2021-09-11 12:02:24 -07:00
Dane Everitt
fde0660e6c
Return tests to passing state 2021-09-11 12:02:15 -07:00
Alex
a775f3ccf2
egg(ark): Fix inverted OR operation (#3605) 2021-09-11 11:45:48 -07:00
Dane Everitt
e96ead4c4d
Update API calls to Wings to only pass the required details with the changes to the installer system 2021-08-29 14:09:43 -07:00
Dane Everitt
869bc22103
Update CHANGELOG.md 2021-08-29 13:42:49 -07:00
Dane Everitt
2d47f986ee
Replace calls to server patch with a manual sync method 2021-08-29 13:32:55 -07:00
Dane Everitt
d8d1eacb42
Don't require Wings API call to pass in order to update server details 2021-08-29 13:19:24 -07:00
Matthew Penner
7330a747b7
migrations: add foreign keys for mount relations (#3574) 2021-08-24 13:12:35 -06:00
Josh Miles
bd271e2e62
Consistency in Java version modal (#3569) 2021-08-24 12:42:58 -06:00
Alex
e1a667aaaf
Disable blank issues (#3566) 2021-08-24 11:35:30 -06:00
Omar Kamel
b67aceb685
Add environment variable for per_schedule_task_limit (#3557)
The environment variable `PTERODACTYL_PER_SCHEDULE_TASK_LIMIT` can be used to change the maximum number of tasks per schedule.
2021-08-20 11:07:05 -06:00
Matthew Penner
b4cae916ac
transfers: fix allocation array merging logic (#3551) 2021-08-18 12:58:41 -06:00
84 changed files with 1049 additions and 836 deletions

View File

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

View File

@ -1,4 +1,4 @@
blank_issues_enabled: true
blank_issues_enabled: false
contact_links:
- name: Installation Help
url: https://discord.gg/pterodactyl

View File

@ -33,7 +33,7 @@ Note: If your `APP_URL` starts with `https://` you need to provide an `LETSENCRY
| ------------------- | ------------------------------------------------------------------------------ | -------- |
| `APP_URL` | The URL the panel will be reachable with (including protocol) | yes |
| `APP_TIMEZONE` | The timezone to use for the panel | yes |
| `LETSENCRYPT_EMAIL` | The email used for letsencrypt certificate generation | yes |
| `LE_EMAIL` | The email used for letsencrypt certificate generation | yes |
| `DB_HOST` | The host of the mysql instance | yes |
| `DB_PORT` | The port of the mysql instance | yes |
| `DB_DATABASE` | The name of the mysql database | yes |

View File

@ -30,7 +30,7 @@ else
fi
echo "Checking if https is required."
if [ -f /etc/nginx/conf.d/default.conf ]; then
if [ -f /etc/nginx/http.d/panel.conf ]; then
echo "Using nginx config already in place."
if [ $LE_EMAIL ]; then
echo "Checking for cert update"
@ -42,20 +42,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

View File

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

View File

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

View File

@ -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.

View File

@ -2,7 +2,7 @@
# Build the assets that are needed for the frontend. This build stage is then discarded
# since we won't need NodeJS anymore in the future. This Docker image ships a final production
# level distribution of Pterodactyl.
FROM mhart/alpine-node:14
FROM --platform=$TARGETOS/$TARGETARCH mhart/alpine-node:14
WORKDIR /app
COPY . ./
RUN yarn install --frozen-lockfile \
@ -10,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

View File

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

View File

@ -5,18 +5,13 @@ The following versions of Pterodactyl are receiving active support and maintenan
| Panel | Daemon | Supported |
| ----- | ------------ | ------------------ |
| 1.4.x | wings@1.4.x | :white_check_mark: |
| 1.3.x | wings@1.3.x | :x: |
| 1.2.x | wings@1.2.x | :x: |
| 1.1.x | wings@1.1.x | :x: |
| 1.0.x | wings@1.0.x | :x: |
| 1.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.

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ use Pterodactyl\Models\User;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\Events\Failed;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Container\Container;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\Authenticatable;
@ -17,6 +17,8 @@ abstract class AbstractLoginController extends Controller
{
use AuthenticatesUsers;
protected AuthManager $auth;
/**
* Lockout time for failed login requests.
*
@ -38,26 +40,14 @@ abstract class AbstractLoginController extends Controller
*/
protected $redirectTo = '/';
/**
* @var \Illuminate\Auth\AuthManager
*/
protected $auth;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
protected $config;
/**
* LoginController constructor.
*/
public function __construct(AuthManager $auth, Repository $config)
public function __construct()
{
$this->lockoutTime = $config->get('auth.lockout.time');
$this->maxLoginAttempts = $config->get('auth.lockout.attempts');
$this->auth = $auth;
$this->config = $config;
$this->lockoutTime = config('auth.lockout.time');
$this->maxLoginAttempts = config('auth.lockout.attempts');
$this->auth = Container::getInstance()->make(AuthManager::class);
}
/**
@ -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
{

View File

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

View File

@ -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' => [

View File

@ -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,
];

View File

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

View 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

View File

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

View File

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

View File

@ -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,
],

View File

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

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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
View File

@ -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": "*"
},

View File

@ -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' => [

View File

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

View File

@ -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"

View File

@ -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"

View File

@ -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": {

View File

@ -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"
}
]
}
}

View File

@ -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,
@ -119,4 +119,4 @@
"rules": "nullable|string"
}
]
}
}

View File

@ -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",

View File

@ -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",
@ -83,4 +82,4 @@
"rules": "nullable|string"
}
]
}
}

View File

@ -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"
}
]
}

View File

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

View File

@ -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']);
});
}
}

View File

@ -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']);
});
}
}

View File

@ -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']);
});
}
}

View File

@ -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:

View File

@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = () => {
]}
/>
&nbsp;{!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`}>

View File

@ -97,6 +97,7 @@ export default () => {
setCpu(
new Chart(node.getContext('2d')!, chartDefaults({
callback: (value) => `${value}% `,
suggestedMax: limits.cpu,
})),
);
}, []);

View 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</>;
};

View File

@ -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`}>

View File

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

View File

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

View File

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

View File

@ -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`}>

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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.',

View File

@ -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 () {

View File

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

View File

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

View File

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

View File

@ -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.
*/

View File

@ -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',
],
],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"